├── .babelrc ├── .editorconfig ├── .gitignore ├── .travis.yml ├── .vscode ├── launch.json └── tasks.json ├── LICENSE ├── README.md ├── appveyor.yml ├── bower.json ├── index.d.ts ├── index.ts ├── package.json ├── scripts └── build-bower.js ├── src ├── applyDecorators.js ├── autobind.js ├── core-decorators.js ├── debounce.js ├── decorate.js ├── deprecate.js ├── enumerable.js ├── extendDescriptor.js ├── lazy-initialize.js ├── memoize.js ├── mixin.js ├── nonconfigurable.js ├── nonenumerable.js ├── override.js ├── private │ └── utils.js ├── profile.js ├── readonly.js ├── suppress-warnings.js ├── throttle.js └── time.js ├── test ├── exports.spec.js ├── test.spec.js └── unit │ ├── applyDecorators.spec.js │ ├── autobind.spec.js │ ├── debounce.spec.js │ ├── decorate.spec.js │ ├── deprecate.spec.js │ ├── enumerable.spec.js │ ├── extendDescriptor.js │ ├── lazy-initialize.spec.js │ ├── memoize.spec.js │ ├── mixin.spec.js │ ├── nonconfigurable.spec.js │ ├── nonenumerable.spec.js │ ├── override.spec.js │ ├── profile.spec.js │ ├── readonly.spec.js │ ├── suppress-warnings.spec.js │ ├── throttle.spec.js │ └── time.spec.js └── tsconfig.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["es2015", { "modules": false }]], 3 | "plugins": [ 4 | "transform-object-rest-spread", 5 | "transform-flow-strip-types", 6 | "transform-decorators-legacy", 7 | "transform-class-properties" 8 | ], 9 | "env": { 10 | "es": {}, 11 | "development": { 12 | "plugins": [ 13 | "transform-es2015-modules-commonjs" 14 | ] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://EditorConfig.org 2 | 3 | # Top-most EditorConfig file. 4 | root = true 5 | 6 | # Common config. 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespaces = true 12 | 13 | # JavaScript 14 | # 15 | # Two spaces seems to be the standard most common style, at least in 16 | # Node.js (http://nodeguide.com/style.html#tabs-vs-spaces). 17 | [*.js] 18 | indent_size = 2 19 | indent_style = space 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /lib/ 2 | /es/ 3 | /node_modules/ 4 | /built/ 5 | *.log 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Mocha Tests", 11 | "program": "${workspaceRoot}/node_modules/mocha/bin/_mocha", 12 | "args": [ 13 | "--compilers", "js:babel-core/register", 14 | "--require", "babel-polyfill", 15 | "${workspaceRoot}/test/**/*.spec.js", 16 | "--debug" 17 | ] 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "0.1.0", 5 | "command": "tsc", 6 | "isShellCommand": true, 7 | "args": ["-w", "-p", "."], 8 | "showOutput": "silent", 9 | "isBackground": true, 10 | "problemMatcher": "$tsc-watch" 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2018 Jay Phelps 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **WARNING: this library was made using the [JavaScript stage-0 decorators](https://github.com/wycats/javascript-decorators) spec, not the latest version (stage-2) which drastically changed in breaking ways.** As such, it was and still is highly experimental and at this point probably best avoided. If/when the decorators spec ever becomes stage-3, and at least one JS compiler supports it, this repo will be updated to support that specification and then we can work towards a stable v1.0.0 version. 2 | > **In the meantime, this repo should mostly considered unmaintained**, except for any security/critical issues, as any work done would mostly be thrown away. 3 | 4 | # core-decorators.js [![Build Status](https://travis-ci.org/jayphelps/core-decorators.svg?branch=master)](https://travis-ci.org/jayphelps/core-decorators) 5 | 6 | Library of [JavaScript stage-0 decorators](https://github.com/wycats/javascript-decorators) (aka ES2016/ES7 decorators [but that's not accurate](https://medium.com/@jayphelps/please-stop-referring-to-proposed-javascript-features-as-es7-cad29f9dcc4b)) inspired by languages that come with built-ins like @​override, @​deprecate, @​autobind, @​mixin and more. Popular with React/Angular, but is framework agnostic. Similar to [Annotations in Java](https://docs.oracle.com/javase/tutorial/java/annotations/predefined.html) but unlike Java annotations, decorators are functions which are applied at runtime. 7 | 8 | These are stage-0 decorators because while [the decorators spec has changed](http://tc39.github.io/proposal-decorators/) and is now stage-2, no transpiler has yet to implement these changes and until they do, this library won't either. Although the [TypeScript documentation](http://www.typescriptlang.org/docs/handbook/decorators.html) uses the phrase "Decorators are a stage 2 proposal for JavaScript" this is misleading because TypeScript still only implements the **stage-0** version of the spec, which is very incompatible with stage-2 (as of this writing). If you concretely find that a compiler (babel, TS, etc) implement stage-2+, please do link me to the appropriate release notes! :balloon: 9 | 10 | _*compiled code is intentionally not checked into this repo_ 11 | 12 | ### Get It 13 | 14 | A version compiled to ES5 in CJS format is published to npm as [`core-decorators`](https://www.npmjs.com/package/core-decorators) 15 | ```bash 16 | npm install core-decorators --save 17 | ``` 18 | 19 | This can be consumed by any transpiler that supports stage-0 of the decorators spec, like [babel.js](https://babeljs.io/) version 5. *Babel 6 [does not yet support decorators natively](https://phabricator.babeljs.io/T2645), but you can include [babel-plugin-transform-decorators-legacy](https://github.com/loganfsmyth/babel-plugin-transform-decorators-legacy) or use the [`applyDecorators()` helper](#applydecorators-helper).* 20 | 21 | core-decorators does not officially support TypeScript. There are known incompatibilities with the way it transpiles the output. PRs certainly welcome to fix that! 22 | 23 | ##### Bower/globals 24 | 25 | A globals version is available [here in the artifact repo](https://github.com/jayphelps/core-decorators-artifacts), or via `$ bower install core-decorators`. It defines a global variable `CoreDecorators`, which can then be used as you might expect: `@CoreDecorators.autobind()`, etc. 26 | 27 | I *highly* recommend against using that globals build as it's quite strange you're using decorators (a proposed future feature of JavaScript) while not using ES2015 modules, a spec ratified feature used by nearly every modern framework. Also--[bower is on its deathbed](https://github.com/bower/bower/pull/1748) and IMO for very good reasons. 28 | 29 | ## Need lodash utilities as decorators? 30 | 31 | core-decorators aims to provide decorators that are fundamental to JavaScript itself--mostly things you could do with normal `Object.defineProperty` but not as easily when using ES2015 classes. Things like debouncing, throttling, and other more opinionated decorators are being phased out in favor of [lodash-decorators](https://www.npmjs.com/package/lodash-decorators) which wraps applicable lodash utilities as decorators. We don't want to duplicate the effort of lodash, which has years and years of robust testing and bugfixes. 32 | 33 | ## Decorators 34 | 35 | ##### For Properties and Methods 36 | * [@readonly](#readonly) 37 | * [@nonconfigurable](#nonconfigurable) 38 | * [@decorate](#decorate) 39 | * [@extendDescriptor](#extenddescriptor) 40 | 41 | ##### For Properties 42 | * [@nonenumerable](#nonenumerable) 43 | * [@lazyInitialize](#lazyinitialize) 44 | 45 | ##### For Methods 46 | * [@autobind](#autobind) 47 | * [@deprecate](#deprecate-alias-deprecated) 48 | * [@suppressWarnings](#suppresswarnings) 49 | * [@enumerable](#enumerable) 50 | * [@override](#override) 51 | * [@debounce](#debounce) :no_entry_sign: DEPRECATED 52 | * [@throttle](#throttle) :no_entry_sign: DEPRECATED 53 | * [@time](#time) 54 | * [@profile](#profile) 55 | 56 | ##### For Classes 57 | * [@autobind](#autobind) 58 | * [@mixin](#mixin-alias-mixins) :no_entry_sign: DEPRECATED 59 | 60 | ## Helpers 61 | 62 | * [applyDecorators()](#applydecorators-helper) 63 | 64 | ## Docs 65 | 66 | ### @autobind 67 | 68 | > Note: there is a bug in `react-hot-loader <= 1.3.0` (they fixed in [`2.0.0-alpha-4`](https://github.com/gaearon/react-hot-loader/pull/182)) which prevents this from working as expected. [Follow it here](https://github.com/jayphelps/core-decorators.js/issues/48) 69 | 70 | Forces invocations of this function to always have `this` refer to the class instance, even if the function is passed around or would otherwise lose its `this` context. e.g. `var fn = context.method;` Popular with React components. 71 | 72 | Individual methods: 73 | 74 | ```js 75 | import { autobind } from 'core-decorators'; 76 | 77 | class Person { 78 | @autobind 79 | getPerson() { 80 | return this; 81 | } 82 | } 83 | 84 | let person = new Person(); 85 | let { getPerson } = person; 86 | 87 | getPerson() === person; 88 | // true 89 | ``` 90 | 91 | Entire Class: 92 | 93 | ```js 94 | import { autobind } from 'core-decorators'; 95 | 96 | @autobind 97 | class Person { 98 | getPerson() { 99 | return this; 100 | } 101 | 102 | getPersonAgain() { 103 | return this; 104 | } 105 | } 106 | 107 | let person = new Person(); 108 | let { getPerson, getPersonAgain } = person; 109 | 110 | getPerson() === person; 111 | // true 112 | 113 | getPersonAgain() === person; 114 | // true 115 | ``` 116 | ### @readonly 117 | 118 | Marks a property or method as not being writable. 119 | 120 | ```js 121 | import { readonly } from 'core-decorators'; 122 | 123 | class Meal { 124 | @readonly 125 | entree = 'steak'; 126 | } 127 | 128 | var dinner = new Meal(); 129 | dinner.entree = 'salmon'; 130 | // Cannot assign to read only property 'entree' of [object Object] 131 | 132 | ``` 133 | 134 | ### @override 135 | 136 | Checks that the marked method indeed overrides a function with the same signature somewhere on the prototype chain. 137 | 138 | Works with methods and getters/setters only (**not** property initializers/arrow functions). Will ensure name, parameter count, as well as descriptor type (accessor/data). Provides a suggestion if it finds a method with a similar signature, including slight misspellings. 139 | 140 | ```js 141 | import { override } from 'core-decorators'; 142 | 143 | class Parent { 144 | speak(first, second) {} 145 | } 146 | 147 | class Child extends Parent { 148 | @override 149 | speak() {} 150 | // SyntaxError: Child#speak() does not properly override Parent#speak(first, second) 151 | } 152 | 153 | // or 154 | 155 | class Child extends Parent { 156 | @override 157 | speaks() {} 158 | // SyntaxError: No descriptor matching Child#speaks() was found on the prototype chain. 159 | // 160 | // Did you mean "speak"? 161 | } 162 | ``` 163 | 164 | ### @deprecate (alias: @deprecated) 165 | 166 | Calls `console.warn()` with a deprecation message. Provide a custom message to override the default one. You can also provide an options hash with a `url`, for further reading. 167 | 168 | ```js 169 | import { deprecate } from 'core-decorators'; 170 | 171 | class Person { 172 | @deprecate 173 | facepalm() {} 174 | 175 | @deprecate('We stopped facepalming') 176 | facepalmHard() {} 177 | 178 | @deprecate('We stopped facepalming', { url: 'http://knowyourmeme.com/memes/facepalm' }) 179 | facepalmHarder() {} 180 | } 181 | 182 | let person = new Person(); 183 | 184 | person.facepalm(); 185 | // DEPRECATION Person#facepalm: This function will be removed in future versions. 186 | 187 | person.facepalmHard(); 188 | // DEPRECATION Person#facepalmHard: We stopped facepalming 189 | 190 | person.facepalmHarder(); 191 | // DEPRECATION Person#facepalmHarder: We stopped facepalming 192 | // 193 | // See http://knowyourmeme.com/memes/facepalm for more details. 194 | // 195 | ``` 196 | 197 | ### @debounce :no_entry_sign: DEPRECATED 198 | 199 | Creates a new debounced function which will be invoked after `wait` milliseconds since the time it was invoked. Default timeout is 300 ms. 200 | 201 | Optional boolean second argument allows to trigger function on the leading instead of the trailing edge of the wait interval. Implementation is inspired by similar method from [UnderscoreJS](http://underscorejs.org/#debounce). 202 | 203 | ```js 204 | import { debounce } from 'core-decorators'; 205 | 206 | class Editor { 207 | 208 | content = ''; 209 | 210 | @debounce(500) 211 | updateContent(content) { 212 | this.content = content; 213 | } 214 | } 215 | ``` 216 | 217 | ### @throttle :no_entry_sign: DEPRECATED 218 | 219 | Creates a new throttled function which will be invoked in every `wait` milliseconds. Default timeout is 300 ms. 220 | 221 | Second argument is optional options: 222 | 223 | - `leading`: default to `true`, allows to trigger function on the leading. 224 | - `trailing`: default to `true`, allows to trigger function on the trailing edge of the wait interval. 225 | 226 | Implementation is inspired by similar method from [UnderscoreJS](http://underscorejs.org/#throttle). 227 | 228 | ```js 229 | import { throttle } from 'core-decorators'; 230 | 231 | class Editor { 232 | 233 | content = ''; 234 | 235 | @throttle(500, {leading: false}) 236 | updateContent(content) { 237 | this.content = content; 238 | } 239 | } 240 | ``` 241 | 242 | ### @suppressWarnings 243 | 244 | Suppresses any JavaScript `console.warn()` call while the decorated function is called. (i.e. on the stack) 245 | 246 | Will _not_ suppress warnings triggered in any async code within. 247 | 248 | ```js 249 | import { suppressWarnings } from 'core-decorators'; 250 | 251 | class Person { 252 | @deprecated 253 | facepalm() {} 254 | 255 | @suppressWarnings 256 | facepalmWithoutWarning() { 257 | this.facepalm(); 258 | } 259 | } 260 | 261 | let person = new Person(); 262 | 263 | person.facepalmWithoutWarning(); 264 | // no warning is logged 265 | ``` 266 | 267 | ### @enumerable 268 | 269 | Marks a method as being enumerable. Note that instance properties are _already_ enumerable, so this is only useful for methods. 270 | 271 | ```js 272 | import { enumerable } from 'core-decorators'; 273 | 274 | class Meal { 275 | pay() {} 276 | 277 | @enumerable 278 | eat() {} 279 | } 280 | 281 | var dinner = new Meal(); 282 | for (var key in dinner) { 283 | key; 284 | // "eat" only, not "pay" 285 | } 286 | 287 | ``` 288 | 289 | ### @nonenumerable 290 | 291 | Marks a property as not being enumerable. Note that class methods are _already_ nonenumerable, so this is only useful for instance properties. 292 | 293 | ```js 294 | import { nonenumerable } from 'core-decorators'; 295 | 296 | class Meal { 297 | entree = 'steak'; 298 | 299 | @nonenumerable 300 | cost = 20.99; 301 | } 302 | 303 | var dinner = new Meal(); 304 | for (var key in dinner) { 305 | key; 306 | // "entree" only, not "cost" 307 | } 308 | 309 | Object.keys(dinner); 310 | // ["entree"] 311 | 312 | ``` 313 | 314 | ### @nonconfigurable 315 | 316 | Marks a property or method so that it cannot be deleted; also prevents it from being reconfigured via `Object.defineProperty`, but **this may not always work how you expect** due to a quirk in JavaScript itself, not this library. Adding the `@readonly` decorator fixes it, but at the cost of obviously making the property readonly (aka `writable: false`). [You can read more about this here.](https://github.com/jayphelps/core-decorators.js/issues/58) 317 | 318 | ```js 319 | import { nonconfigurable } from 'core-decorators'; 320 | 321 | class Foo { 322 | @nonconfigurable 323 | @readonly 324 | bar() {}; 325 | } 326 | 327 | Object.defineProperty(Foo.prototype, 'bar', { 328 | value: 'I will error' 329 | }); 330 | // Cannot redefine property: bar 331 | 332 | ``` 333 | 334 | ### @decorate 335 | 336 | Immediately applies the provided function and arguments to the method, allowing you to wrap methods with arbitrary helpers like [those provided by lodash](https://lodash.com/docs#after). The first argument is the function to apply, all further arguments will be passed to that decorating function. 337 | 338 | ```js 339 | import { decorate } from 'core-decorators'; 340 | import { memoize } from 'lodash'; 341 | 342 | var count = 0; 343 | 344 | class Task { 345 | @decorate(memoize) 346 | doSomethingExpensive(data) { 347 | count++; 348 | // something expensive; 349 | return data; 350 | } 351 | } 352 | 353 | var task = new Task(); 354 | var data = [1, 2, 3]; 355 | 356 | task.doSomethingExpensive(data); 357 | task.doSomethingExpensive(data); 358 | 359 | count === 1; 360 | // true 361 | ``` 362 | 363 | ### @lazyInitialize 364 | 365 | Prevents a property initializer from running until the decorated property is actually looked up. Useful to prevent excess allocations that might otherwise not be used, but be careful not to over-optimize things. 366 | 367 | ```js 368 | import { lazyInitialize } from 'core-decorators'; 369 | 370 | function createHugeBuffer() { 371 | console.log('huge buffer created'); 372 | return new Array(1000000); 373 | } 374 | 375 | class Editor { 376 | @lazyInitialize 377 | hugeBuffer = createHugeBuffer(); 378 | } 379 | 380 | var editor = new Editor(); 381 | // createHugeBuffer() has not been called yet 382 | 383 | editor.hugeBuffer; 384 | // logs 'huge buffer created', now it has been called 385 | 386 | editor.hugeBuffer; 387 | // already initialized and equals our buffer, so 388 | // createHugeBuffer() is not called again 389 | ``` 390 | 391 | ### @mixin (alias: @mixins) :no_entry_sign: DEPRECATED 392 | 393 | Mixes in all property descriptors from the provided Plain Old JavaScript Objects (aka POJOs) as arguments. Mixins are applied in the order they are passed, but do **not** override descriptors already on the class, including those inherited traditionally. 394 | 395 | ```js 396 | import { mixin } from 'core-decorators'; 397 | 398 | const SingerMixin = { 399 | sing(sound) { 400 | alert(sound); 401 | } 402 | }; 403 | 404 | const FlyMixin = { 405 | // All types of property descriptors are supported 406 | get speed() {}, 407 | fly() {}, 408 | land() {} 409 | }; 410 | 411 | @mixin(SingerMixin, FlyMixin) 412 | class Bird { 413 | singMatingCall() { 414 | this.sing('tweet tweet'); 415 | } 416 | } 417 | 418 | var bird = new Bird(); 419 | bird.singMatingCall(); 420 | // alerts "tweet tweet" 421 | 422 | ``` 423 | 424 | ### @time 425 | 426 | Uses `console.time` and `console.timeEnd` to provide function timings with a unique label whose default prefix is `ClassName.method`. Supply a first argument to override the prefix: 427 | 428 | ```js 429 | class Bird { 430 | @time('sing') 431 | sing() { 432 | } 433 | } 434 | 435 | var bird = new Bird(); 436 | bird.sing(); // console.time label will be 'sing-0' 437 | bird.sing(); // console.time label will be 'sing-1' 438 | ``` 439 | 440 | Will polyfill `console.time` if the current environment does not support it. You can also supply a custom `console` object as the second argument with the following methods: 441 | 442 | * `myConsole.time(label)` 443 | * `myConsole.timeEnd(label)` 444 | * `myConsole.log(value)` 445 | 446 | ```js 447 | let myConsole = { 448 | time: function(label) { /* custom time() method */ }, 449 | timeEnd: function(label) { /* custom timeEnd method */ }, 450 | log: function(str) { /* custom log method */ } 451 | } 452 | ``` 453 | 454 | ### @profile 455 | 456 | Uses `console.profile` and `console.profileEnd` to provide function profiling with a unique label whose default prefix is `ClassName.method`. Supply a first argument to override the prefix: 457 | 458 | ```js 459 | class Bird { 460 | @profile('sing') 461 | sing() { 462 | } 463 | } 464 | 465 | var bird = new Bird(); 466 | bird.sing(); // Adds a profile with label sing and marked as run 1 467 | bird.sing(); // Adds a profile with label sing and marked as run 2 468 | ``` 469 | 470 | Because profiling is expensive, you may not want to run it every time the function is called. Supply a second argument of `true` to have the profiling only run once: 471 | 472 | ```js 473 | class Bird { 474 | @profile(null, true) 475 | sing() { 476 | } 477 | } 478 | 479 | var bird = new Bird(); 480 | bird.sing(); // Adds a profile with label Bird.sing 481 | bird.sing(); // Does nothing 482 | ``` 483 | 484 | Alternatively you can pass a number instead of `true` to represent the milliseconds between profiles. Profiling is always ran on the leading edge. 485 | 486 | ```js 487 | class Bird { 488 | @profile(null, 1000) 489 | sing() { 490 | } 491 | } 492 | 493 | var bird = new Bird(); 494 | bird.sing(); // Adds a profile with label Bird.sing 495 | // Wait 100ms 496 | bird.sing(); // Does nothing 497 | // Wait 1000ms 498 | bird.sing(); // Adds a profile with label Bird.sing 499 | ``` 500 | 501 | When you need extremely fine-tuned control, you can pass a function that returns a boolean to determine if profiling should occur. The function will have `this` context of the instance and the arguments to the method will be passed to the function as well. Arrow functions **will not** receive the instance context. 502 | 503 | ```js 504 | class Bird { 505 | @profile(null, function (volume) { return volume === 'loud'; }) 506 | sing(volume) { 507 | } 508 | 509 | @profile(null, function () { return this.breed === 'eagle' }) 510 | fly() { 511 | } 512 | } 513 | 514 | var bird = new Bird(); 515 | bird.sing('loud'); // Adds a profile with label Bird.sing 516 | bird.sing('quite'); // Does nothing 517 | 518 | bird.fly(); // Does nothing 519 | bird.breed = 'eagle'; 520 | bird.fly(); // Adds a profile with label Bird.fly 521 | ``` 522 | 523 | Profiling is currently only supported in Chrome 53+, Firefox, and Edge. Unfortunately this feature can't be polyfilled or faked, so if used in an unsupported browser or Node.js then this decorator will automatically disable itself. 524 | 525 | ### @extendDescriptor 526 | 527 | Extends the new property descriptor with the descriptor from the super/parent class prototype. Although useful in various circumstances, it's particularly helpful to address the fact that getters and setters share a single descriptor so overriding only a getter or only a setter will blow away the other, without this decorator. 528 | 529 | ```js 530 | class Base { 531 | @nonconfigurable 532 | get foo() { 533 | return `hello ${this._foo}`; 534 | } 535 | } 536 | 537 | class Derived extends Base { 538 | @extendDescriptor 539 | set foo(value) { 540 | this._foo = value; 541 | } 542 | } 543 | 544 | const derived = new Derived(); 545 | derived.foo = 'bar'; 546 | derived.foo === 'hello bar'; 547 | // true 548 | 549 | const desc = Object.getOwnPropertyDescriptor(Derived.prototype, 'foo'); 550 | desc.configurable === false; 551 | // true 552 | ``` 553 | 554 | ### applyDecorators() helper 555 | 556 | The `applyDecorators()` helper can be used when you don't have language support for decorators like in Babel 6 or even with vanilla ES5 code without a transpiler. 557 | 558 | ```js 559 | class Foo { 560 | getFoo() { 561 | return this; 562 | } 563 | } 564 | 565 | // This works on regular function prototypes 566 | // too, like `function Foo() {}` 567 | applyDecorators(Foo, { 568 | getFoo: [autobind] 569 | }); 570 | 571 | let foo = new Foo(); 572 | let getFoo = foo.getFoo; 573 | getFoo() === foo; 574 | // true 575 | ``` 576 | 577 | # Future Compatibility 578 | Since most people can't keep up to date with specs, it's important to note that the spec is in-flux and subject to breaking changes. For the most part, these changes will _probably_ be transparent to consumers of this project--that said, core-decorators has not yet reached 1.0 and may in fact introduce breaking changes. If you'd prefer not to receive these changes, be sure to lock your dependency to [PATCH](http://semver.org/). You can track the progress of core-decorators@1.0.0 in the [The Road to 1.0](https://github.com/jayphelps/core-decorators.js/issues/15) ticket. 579 | 580 | # Decorator Order Sometimes Matters 581 | When using multiple decorators on a class, method, or property the order of the decorators sometimes matters. This is a neccesary caveat of decorators because otherwise certain cool features wouldn't be possible. The most common example of this is using `@autobind` and any [Higher-Order Component (HOC)](https://medium.com/@dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750) decorator, e.g. Redux's `@connect`. You must `@autobind` your class first before applying the `@connect` HOC. 582 | 583 | ```js 584 | @connect() 585 | @autobind 586 | class Foo extends Component {} 587 | ``` 588 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Configuration for CI under Windows at http://appveyor.com 2 | 3 | # Test against the latest version of this Node.js version 4 | environment: 5 | nodejs_version: "6" 6 | 7 | # Install scripts. (runs after repo cloning) 8 | install: 9 | # Get the latest stable version of Node.js or io.js 10 | - ps: Install-Product node $env:nodejs_version 11 | # install modules 12 | - npm install 13 | 14 | # Post-install test scripts. 15 | test_script: 16 | # Output useful info for debugging. 17 | - node --version 18 | - npm --version 19 | # run tests 20 | - npm test 21 | 22 | # Don't actually build. 23 | build: off 24 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "core-decorators.js", 3 | "description": "Library of ES2016 (ES7) JavaScript decorators inspired by languages that come with built-ins like @​override, @​deprecate, @​autobind, @​mixin and more! Works great with React/Angular/more!", 4 | "version": "0.11.2", 5 | "homepage": "https://github.com/jayphelps/core-decorators.js", 6 | "authors": [ 7 | "Jay Phelps " 8 | ], 9 | "main": "core-decorators.js", 10 | "moduleType": [ 11 | "es6", 12 | "globals", 13 | "node" 14 | ], 15 | "license": "MIT", 16 | "keywords": [ 17 | "es6", 18 | "es7", 19 | "es2015", 20 | "es2016", 21 | "babel", 22 | "decorators", 23 | "override", 24 | "deprecated", 25 | "java", 26 | "annotations", 27 | "autobind", 28 | "react", 29 | "angular", 30 | "lodash", 31 | "mixin", 32 | "mixins" 33 | ], 34 | "ignore": [ 35 | "**/.*", 36 | "node_modules", 37 | "test", 38 | "scripts", 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for core-decorators.js 0.19 2 | // Project: https://github.com/jayphelps/core-decorators.js 3 | // Definitions by: Qubo 4 | // dtweedle 5 | // TypeScript Version: 2.4.1 6 | 7 | export interface PropertyOrMethodDecorator extends MethodDecorator, PropertyDecorator { 8 | (target: object, propertyKey: string | symbol): void; 9 | } 10 | 11 | export interface Deprecate extends MethodDecorator { 12 | (message?: string, option?: DeprecateOption): MethodDecorator; 13 | } 14 | 15 | export interface DeprecateOption { 16 | url: string; 17 | } 18 | 19 | /** 20 | * Forces invocations of this function to always have this refer to the class instance, 21 | * even if the function is passed around or would otherwise lose its this context. e.g. var fn = context.method; 22 | */ 23 | export const autobind: Function; 24 | /** 25 | * Marks a property or method as not being writable. 26 | */ 27 | export const readonly: PropertyOrMethodDecorator; 28 | /** 29 | * Checks that the marked method indeed overrides a function with the same signature somewhere on the prototype chain. 30 | */ 31 | export const override: MethodDecorator; 32 | /** 33 | * Calls console.warn() with a deprecation message. Provide a custom message to override the default one. You can also provide an options hash with a url, for further reading. 34 | */ 35 | export const deprecate: Deprecate; 36 | /** 37 | * Calls console.warn() with a deprecation message. Provide a custom message to override the default one. You can also provide an options hash with a url, for further reading. 38 | */ 39 | export const deprecated: Deprecate; 40 | /** 41 | * Suppresses any JavaScript console.warn() call while the decorated function is called. (i.e. on the stack) 42 | */ 43 | export const suppressWarnings: MethodDecorator; 44 | /** 45 | * Marks a property or method as not being enumerable. 46 | */ 47 | export const nonenumerable: PropertyOrMethodDecorator; 48 | /** 49 | * Marks a property or method as not being writable. 50 | */ 51 | export const nonconfigurable: PropertyOrMethodDecorator; 52 | /** 53 | * Initial implementation included, likely slow. WIP. 54 | */ 55 | export const memoize: MethodDecorator; 56 | /** 57 | * Immediately applies the provided function and arguments to the method, allowing you to wrap methods with arbitrary helpers like those provided by lodash. 58 | * The first argument is the function to apply, all further arguments will be passed to that decorating function. 59 | */ 60 | export function decorate(func: Function, ...args: any[]): MethodDecorator; 61 | /** 62 | * Prevents a property initializer from running until the decorated property is actually looked up. 63 | * Useful to prevent excess allocations that might otherwise not be used, but be careful not to over-optimize things. 64 | */ 65 | export const lazyInitialize: PropertyDecorator; 66 | /** 67 | * Uses console.time and console.timeEnd to provide function timings with a unique label whose default prefix is ClassName.method. Supply a first argument to override the prefix: 68 | */ 69 | export function time(label: string, console?: Console): MethodDecorator; 70 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | // index.ts - facade for core-decorators 2 | // 3 | 4 | export * from './lib/core-decorators'; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "core-decorators", 3 | "version": "0.20.0", 4 | "description": "Library of JavaScript stage-0 decorators (aka ES2016/ES7 decorators but that's not accurate!) inspired by languages that come with built-ins like @​override, @​deprecate, @​autobind, @​mixin and more! Works great with React/Angular/more!", 5 | "main": "lib/core-decorators.js", 6 | "module": "es/core-decorators.js", 7 | "jsnext:main": "es/core-decorators.js", 8 | "typings": "./index.d.ts", 9 | "files": [ 10 | "es", 11 | "lib", 12 | "src", 13 | "README.md", 14 | "LICENSE", 15 | "index.d.ts" 16 | ], 17 | "scripts": { 18 | "prepublish": "npm run build", 19 | "build": "rimraf lib && babel --out-dir lib src && cross-env BABEL_ENV=es babel --out-dir es src", 20 | "build-bower": "babel-node scripts/build-bower.js", 21 | "prebuild-tsc": "rimraf built", 22 | "build-tsc": "tsc --outDir lib", 23 | "clean": "rimraf lib es built", 24 | "test": "mocha --compilers js:babel-core/register --require babel-polyfill \"test/**/*.spec.js\"" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/jayphelps/core-decorators.js.git" 29 | }, 30 | "keywords": [ 31 | "es6", 32 | "es7", 33 | "es2015", 34 | "es2016", 35 | "babel", 36 | "decorators", 37 | "override", 38 | "deprecated", 39 | "java", 40 | "annotations", 41 | "autobind", 42 | "react", 43 | "angular", 44 | "lodash", 45 | "mixin", 46 | "mixins" 47 | ], 48 | "author": "Jay Phelps ", 49 | "license": "MIT", 50 | "bugs": { 51 | "url": "https://github.com/jayphelps/core-decorators.js/issues" 52 | }, 53 | "homepage": "https://github.com/jayphelps/core-decorators.js", 54 | "devDependencies": { 55 | "@types/chai": "^3.5.2", 56 | "@types/mocha": "^2.2.41", 57 | "@types/node": "^6.0.73", 58 | "babel-cli": "^6.24.0", 59 | "babel-core": "^6.24.0", 60 | "babel-plugin-transform-class-properties": "^6.23.0", 61 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 62 | "babel-plugin-transform-es2015-modules-commonjs": "^6.24.0", 63 | "babel-plugin-transform-flow-strip-types": "^6.22.0", 64 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 65 | "babel-polyfill": "^6.23.0", 66 | "babel-preset-es2015": "^6.24.0", 67 | "camelcase": "^4.1.0", 68 | "chai": "^3.5.0", 69 | "cross-env": "^5.0.0", 70 | "glob": "^7.1.1", 71 | "global-wrap": "^2.0.0", 72 | "interop-require": "1.0.0", 73 | "lodash": "4.17.5", 74 | "mocha": "^3.2.0", 75 | "rimraf": "^2.6.1", 76 | "sinon": "2.1.0", 77 | "sinon-chai": "^2.9.0", 78 | "typescript": "^2.3.3" 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /scripts/build-bower.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import globalWrap from 'global-wrap'; 3 | 4 | globalWrap({ 5 | main: 'lib/core-decorators.js', 6 | global: 'CoreDecorators', 7 | browserifyOptions: { detectGlobals: false } 8 | }, (error, output) => { 9 | if (error) { 10 | console.error(error); 11 | } else { 12 | fs.writeFile('./core-decorators.js', output, error => { 13 | if (error) { 14 | console.error(error); 15 | } else { 16 | console.log('Bower build success'); 17 | } 18 | }); 19 | } 20 | }); -------------------------------------------------------------------------------- /src/applyDecorators.js: -------------------------------------------------------------------------------- 1 | const { defineProperty, getOwnPropertyDescriptor } = Object; 2 | 3 | export default 4 | function applyDecorators(Class, props) { 5 | const { prototype } = Class; 6 | 7 | for (const key in props) { 8 | const decorators = props[key]; 9 | 10 | for (let i = 0, l = decorators.length; i < l; i++) { 11 | const decorator = decorators[i]; 12 | 13 | defineProperty(prototype, key, 14 | decorator( 15 | prototype, 16 | key, 17 | getOwnPropertyDescriptor(prototype, key) 18 | ) 19 | ); 20 | } 21 | } 22 | 23 | return Class; 24 | } 25 | -------------------------------------------------------------------------------- /src/autobind.js: -------------------------------------------------------------------------------- 1 | import { decorate, createDefaultSetter, 2 | getOwnPropertyDescriptors, getOwnKeys, bind } from './private/utils'; 3 | const { defineProperty, getPrototypeOf } = Object; 4 | 5 | let mapStore; 6 | 7 | function getBoundSuper(obj, fn) { 8 | if (typeof WeakMap === 'undefined') { 9 | throw new Error( 10 | `Using @autobind on ${fn.name}() requires WeakMap support due to its use of super.${fn.name}() 11 | See https://github.com/jayphelps/core-decorators.js/issues/20` 12 | ); 13 | } 14 | 15 | if (!mapStore) { 16 | mapStore = new WeakMap(); 17 | } 18 | 19 | if (mapStore.has(obj) === false) { 20 | mapStore.set(obj, new WeakMap()); 21 | } 22 | 23 | const superStore = mapStore.get(obj); 24 | 25 | if (superStore.has(fn) === false) { 26 | superStore.set(fn, bind(fn, obj)); 27 | } 28 | 29 | return superStore.get(fn); 30 | } 31 | 32 | function autobindClass(klass) { 33 | const descs = getOwnPropertyDescriptors(klass.prototype); 34 | const keys = getOwnKeys(descs); 35 | 36 | for (let i = 0, l = keys.length; i < l; i++) { 37 | const key = keys[i]; 38 | const desc = descs[key]; 39 | 40 | if (typeof desc.value !== 'function' || key === 'constructor') { 41 | continue; 42 | } 43 | 44 | defineProperty(klass.prototype, key, autobindMethod(klass.prototype, key, desc)); 45 | } 46 | } 47 | 48 | function autobindMethod(target, key, { value: fn, configurable, enumerable }) { 49 | if (typeof fn !== 'function') { 50 | throw new SyntaxError(`@autobind can only be used on functions, not: ${fn}`); 51 | } 52 | 53 | const { constructor } = target; 54 | 55 | return { 56 | configurable, 57 | enumerable, 58 | 59 | get() { 60 | // Class.prototype.key lookup 61 | // Someone accesses the property directly on the prototype on which it is 62 | // actually defined on, i.e. Class.prototype.hasOwnProperty(key) 63 | if (this === target) { 64 | return fn; 65 | } 66 | 67 | // Class.prototype.key lookup 68 | // Someone accesses the property directly on a prototype but it was found 69 | // up the chain, not defined directly on it 70 | // i.e. Class.prototype.hasOwnProperty(key) == false && key in Class.prototype 71 | if (this.constructor !== constructor && getPrototypeOf(this).constructor === constructor) { 72 | return fn; 73 | } 74 | 75 | // Autobound method calling super.sameMethod() which is also autobound and so on. 76 | if (this.constructor !== constructor && key in this.constructor.prototype) { 77 | return getBoundSuper(this, fn); 78 | } 79 | 80 | const boundFn = bind(fn, this); 81 | 82 | defineProperty(this, key, { 83 | configurable: true, 84 | writable: true, 85 | // NOT enumerable when it's a bound method 86 | enumerable: false, 87 | value: boundFn 88 | }); 89 | 90 | return boundFn; 91 | }, 92 | set: createDefaultSetter(key) 93 | }; 94 | } 95 | 96 | function handle(args) { 97 | if (args.length === 1) { 98 | return autobindClass(...args); 99 | } else { 100 | return autobindMethod(...args); 101 | } 102 | } 103 | 104 | export default function autobind(...args) { 105 | if (args.length === 0) { 106 | return function (...argsClass) { 107 | return handle(argsClass); 108 | }; 109 | } else { 110 | return handle(args); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/core-decorators.js: -------------------------------------------------------------------------------- 1 | /** 2 | * core-decorators.js 3 | * (c) 2017 Jay Phelps and contributors 4 | * MIT Licensed 5 | * https://github.com/jayphelps/core-decorators.js 6 | * @license 7 | */ 8 | export { default as override } from './override'; 9 | export { default as deprecate, default as deprecated } from './deprecate'; 10 | export { default as suppressWarnings } from './suppress-warnings'; 11 | export { default as memoize } from './memoize'; 12 | export { default as autobind } from './autobind'; 13 | export { default as readonly } from './readonly'; 14 | export { default as enumerable } from './enumerable'; 15 | export { default as nonenumerable } from './nonenumerable'; 16 | export { default as nonconfigurable } from './nonconfigurable'; 17 | export { default as debounce } from './debounce'; 18 | export { default as throttle } from './throttle'; 19 | export { default as decorate } from './decorate'; 20 | export { default as mixin, default as mixins } from './mixin'; 21 | export { default as lazyInitialize } from './lazy-initialize'; 22 | export { default as time } from './time'; 23 | export { default as extendDescriptor } from './extendDescriptor'; 24 | export { default as profile } from './profile'; 25 | 26 | // Helper to apply decorators to a class without transpiler support 27 | export { default as applyDecorators } from './applyDecorators'; 28 | -------------------------------------------------------------------------------- /src/debounce.js: -------------------------------------------------------------------------------- 1 | import { decorate, metaFor, internalDeprecation } from './private/utils'; 2 | 3 | const DEFAULT_TIMEOUT = 300; 4 | 5 | function handleDescriptor(target, key, descriptor, [wait = DEFAULT_TIMEOUT, immediate = false]) { 6 | const callback = descriptor.value; 7 | 8 | if (typeof callback !== 'function') { 9 | throw new SyntaxError('Only functions can be debounced'); 10 | } 11 | 12 | return { 13 | ...descriptor, 14 | value() { 15 | const { debounceTimeoutIds } = metaFor(this); 16 | const timeout = debounceTimeoutIds[key]; 17 | const callNow = immediate && !timeout; 18 | const args = arguments; 19 | 20 | clearTimeout(timeout); 21 | 22 | debounceTimeoutIds[key] = setTimeout(() => { 23 | delete debounceTimeoutIds[key]; 24 | if (!immediate) { 25 | callback.apply(this, args); 26 | } 27 | }, wait); 28 | 29 | if (callNow) { 30 | callback.apply(this, args); 31 | } 32 | } 33 | }; 34 | } 35 | 36 | export default function debounce(...args) { 37 | internalDeprecation('@debounce is deprecated and will be removed shortly. Use @debounce from lodash-decorators.\n\n https://www.npmjs.com/package/lodash-decorators'); 38 | return decorate(handleDescriptor, args); 39 | } 40 | -------------------------------------------------------------------------------- /src/decorate.js: -------------------------------------------------------------------------------- 1 | import { decorate as _decorate, createDefaultSetter } from './private/utils'; 2 | const { defineProperty } = Object; 3 | 4 | function handleDescriptor(target, key, descriptor, [decorator, ...args]) { 5 | const { configurable, enumerable, writable } = descriptor; 6 | const originalGet = descriptor.get; 7 | const originalSet = descriptor.set; 8 | const originalValue = descriptor.value; 9 | const isGetter = !!originalGet; 10 | 11 | return { 12 | configurable, 13 | enumerable, 14 | get() { 15 | const fn = isGetter ? originalGet.call(this) : originalValue; 16 | const value = decorator.call(this, fn, ...args); 17 | 18 | if (isGetter) { 19 | return value; 20 | } else { 21 | const desc = { 22 | configurable, 23 | enumerable 24 | }; 25 | 26 | desc.value = value; 27 | desc.writable = writable; 28 | 29 | defineProperty(this, key, desc); 30 | 31 | return value; 32 | } 33 | }, 34 | set: isGetter ? originalSet : createDefaultSetter() 35 | }; 36 | } 37 | 38 | export default function decorate(...args) { 39 | return _decorate(handleDescriptor, args); 40 | } 41 | -------------------------------------------------------------------------------- /src/deprecate.js: -------------------------------------------------------------------------------- 1 | import { decorate, warn } from './private/utils'; 2 | 3 | const DEFAULT_MSG = 'This function will be removed in future versions.'; 4 | 5 | function handleDescriptor(target, key, descriptor, [msg = DEFAULT_MSG, options = {}]) { 6 | if (typeof descriptor.value !== 'function') { 7 | throw new SyntaxError('Only functions can be marked as deprecated'); 8 | } 9 | 10 | const methodSignature = `${target.constructor.name}#${key}`; 11 | 12 | if (options.url) { 13 | msg += `\n\n See ${options.url} for more details.\n\n`; 14 | } 15 | 16 | return { 17 | ...descriptor, 18 | value: function deprecationWrapper() { 19 | warn(`DEPRECATION ${methodSignature}: ${msg}`); 20 | return descriptor.value.apply(this, arguments); 21 | } 22 | }; 23 | } 24 | 25 | export default function deprecate(...args) { 26 | return decorate(handleDescriptor, args); 27 | } 28 | -------------------------------------------------------------------------------- /src/enumerable.js: -------------------------------------------------------------------------------- 1 | import { decorate } from './private/utils'; 2 | 3 | function handleDescriptor(target, key, descriptor) { 4 | descriptor.enumerable = true; 5 | return descriptor; 6 | } 7 | 8 | export default function enumerable(...args) { 9 | return decorate(handleDescriptor, args); 10 | } 11 | -------------------------------------------------------------------------------- /src/extendDescriptor.js: -------------------------------------------------------------------------------- 1 | import { decorate } from './private/utils'; 2 | const { getPrototypeOf, getOwnPropertyDescriptor } = Object; 3 | 4 | function handleDescriptor(target, key, descriptor) { 5 | const superKlass = getPrototypeOf(target); 6 | const superDesc = getOwnPropertyDescriptor(superKlass, key); 7 | 8 | return { 9 | ...superDesc, 10 | value: descriptor.value, 11 | initializer: descriptor.initializer, 12 | get: descriptor.get || superDesc.get, 13 | set: descriptor.set || superDesc.set 14 | }; 15 | } 16 | 17 | export default function extendDescriptor(...args) { 18 | return decorate(handleDescriptor, args); 19 | } 20 | -------------------------------------------------------------------------------- /src/lazy-initialize.js: -------------------------------------------------------------------------------- 1 | import { decorate, createDefaultSetter } from './private/utils'; 2 | const { defineProperty } = Object; 3 | 4 | function handleDescriptor(target, key, descriptor) { 5 | const { configurable, enumerable, initializer, value } = descriptor; 6 | return { 7 | configurable, 8 | enumerable, 9 | 10 | get() { 11 | // This happens if someone accesses the 12 | // property directly on the prototype 13 | if (this === target) { 14 | return; 15 | } 16 | 17 | const ret = initializer ? initializer.call(this) : value; 18 | 19 | defineProperty(this, key, { 20 | configurable, 21 | enumerable, 22 | writable: true, 23 | value: ret 24 | }); 25 | 26 | return ret; 27 | }, 28 | 29 | set: createDefaultSetter(key) 30 | }; 31 | } 32 | 33 | export default function lazyInitialize(...args) { 34 | return decorate(handleDescriptor, args); 35 | } 36 | -------------------------------------------------------------------------------- /src/memoize.js: -------------------------------------------------------------------------------- 1 | import { decorate, internalDeprecation } from './private/utils'; 2 | 3 | function toObject(cache, value) { 4 | if (value === Object(value)) { 5 | return value; 6 | } 7 | return cache[value] || (cache[value] = {}); 8 | } 9 | 10 | function applyAndCache(context, fn, args, cache, signature) { 11 | const ret = fn.apply(context, args); 12 | cache[signature] = ret; 13 | return ret; 14 | } 15 | 16 | function metaForDescriptor(descriptor) { 17 | let fn, wrapKey; 18 | 19 | // This is ugly code, but way faster than other 20 | // ways I tried that *looked* pretty 21 | 22 | if (descriptor.value) { 23 | fn = descriptor.value; 24 | wrapKey = 'value'; 25 | } else if (descriptor.get) { 26 | fn = descriptor.get; 27 | wrapKey = 'get'; 28 | } else if (descriptor.set) { 29 | fn = descriptor.set; 30 | wrapKey = 'set'; 31 | } 32 | 33 | return { fn, wrapKey }; 34 | } 35 | 36 | function handleDescriptor(target, key, descriptor) { 37 | const { fn, wrapKey } = metaForDescriptor(descriptor); 38 | const argumentCache = new WeakMap(); 39 | const signatureCache = Object.create(null); 40 | const primativeRefCache = Object.create(null); 41 | let argumentIdCounter = 0; 42 | 43 | return { 44 | ...descriptor, 45 | [wrapKey]: function memoizeWrapper(...args) { 46 | let signature = '0'; 47 | 48 | for (let i = 0, l = args.length; i < l; i++) { 49 | let arg = args[i]; 50 | let argRef = toObject(primativeRefCache, arg); 51 | let argKey = argumentCache.get(argRef); 52 | 53 | if (argKey === undefined) { 54 | argKey = ++argumentIdCounter; 55 | argumentCache.set(argRef, argKey); 56 | } 57 | 58 | signature += argKey; 59 | } 60 | 61 | return signatureCache[signature] 62 | || applyAndCache(this, fn, arguments, signatureCache, signature); 63 | } 64 | }; 65 | } 66 | 67 | export default function memoize(...args) { 68 | internalDeprecation('@memoize is deprecated and will be removed shortly. Use @memoize from lodash-decorators.\n\n https://www.npmjs.com/package/lodash-decorators'); 69 | return decorate(handleDescriptor, args); 70 | } -------------------------------------------------------------------------------- /src/mixin.js: -------------------------------------------------------------------------------- 1 | import { getOwnPropertyDescriptors, getOwnKeys, internalDeprecation } from './private/utils'; 2 | 3 | const { defineProperty, getPrototypeOf } = Object; 4 | 5 | function buggySymbol(symbol) { 6 | return Object.prototype.toString.call(symbol) === '[object Symbol]' && typeof(symbol) === 'object'; 7 | } 8 | 9 | function hasProperty(prop, obj) { 10 | // We have to traverse manually prototypes' chain for polyfilled ES6 Symbols 11 | // like "in" operator does. 12 | // I.e.: Babel 5 Symbol polyfill stores every created symbol in Object.prototype. 13 | // That's why we cannot use construction like "prop in obj" to check, if needed 14 | // prop actually exists in given object/prototypes' chain. 15 | if (buggySymbol(prop)) { 16 | do { 17 | if (obj === Object.prototype) { 18 | // Polyfill assigns undefined as value for stored symbol key. 19 | // We can assume in this special case if there is nothing assigned it doesn't exist. 20 | return typeof(obj[prop]) !== 'undefined'; 21 | } 22 | if (obj.hasOwnProperty(prop)) { 23 | return true; 24 | } 25 | } while (obj = getPrototypeOf(obj)); 26 | return false; 27 | } else { 28 | return prop in obj; 29 | } 30 | } 31 | 32 | function handleClass(target, mixins) { 33 | if (!mixins.length) { 34 | throw new SyntaxError(`@mixin() class ${target.name} requires at least one mixin as an argument`); 35 | } 36 | 37 | for (let i = 0, l = mixins.length; i < l; i++) { 38 | const descs = getOwnPropertyDescriptors(mixins[i]); 39 | const keys = getOwnKeys(descs); 40 | 41 | for (let j = 0, k = keys.length; j < k; j++) { 42 | const key = keys[j]; 43 | 44 | if (!(hasProperty(key, target.prototype))) { 45 | defineProperty(target.prototype, key, descs[key]); 46 | } 47 | } 48 | } 49 | } 50 | 51 | export default function mixin(...mixins) { 52 | internalDeprecation('@mixin is deprecated and will be removed shortly. Use @mixin from lodash-decorators.\n\n https://www.npmjs.com/package/lodash-decorators'); 53 | 54 | if (typeof mixins[0] === 'function') { 55 | return handleClass(mixins[0], []); 56 | } else { 57 | return target => { 58 | return handleClass(target, mixins); 59 | }; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/nonconfigurable.js: -------------------------------------------------------------------------------- 1 | import { decorate } from './private/utils'; 2 | 3 | function handleDescriptor(target, key, descriptor) { 4 | descriptor.configurable = false; 5 | return descriptor; 6 | } 7 | 8 | export default function nonconfigurable(...args) { 9 | return decorate(handleDescriptor, args); 10 | } 11 | -------------------------------------------------------------------------------- /src/nonenumerable.js: -------------------------------------------------------------------------------- 1 | import { decorate } from './private/utils'; 2 | 3 | function handleDescriptor(target, key, descriptor) { 4 | descriptor.enumerable = false; 5 | return descriptor; 6 | } 7 | 8 | export default function nonenumerable(...args) { 9 | return decorate(handleDescriptor, args); 10 | } 11 | -------------------------------------------------------------------------------- /src/override.js: -------------------------------------------------------------------------------- 1 | import { decorate } from './private/utils'; 2 | 3 | const GENERIC_FUNCTION_ERROR = '{child} does not properly override {parent}'; 4 | const FUNCTION_REGEXP = /^function ([_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*)?(\([^\)]*\))[\s\S]+$/; 5 | 6 | class SyntaxErrorReporter { 7 | parentKlass; 8 | childKlass; 9 | parentDescriptor; 10 | childDescriptor; 11 | 12 | get key() { 13 | return this.childDescriptor.key; 14 | } 15 | 16 | get parentNotation() { 17 | return `${this.parentKlass.constructor.name}#${this.parentPropertySignature}`; 18 | } 19 | 20 | get childNotation() { 21 | return `${this.childKlass.constructor.name}#${this.childPropertySignature}`; 22 | } 23 | 24 | get parentTopic() { 25 | return this._getTopic(this.parentDescriptor); 26 | } 27 | 28 | get childTopic() { 29 | return this._getTopic(this.childDescriptor); 30 | } 31 | 32 | _getTopic(descriptor) { 33 | if (descriptor === undefined) { 34 | return null; 35 | } 36 | 37 | if ('value' in descriptor) { 38 | return descriptor.value; 39 | } 40 | 41 | if ('get' in descriptor) { 42 | return descriptor.get; 43 | } 44 | 45 | if ('set' in descriptor) { 46 | return descriptor.set; 47 | } 48 | } 49 | 50 | get parentPropertySignature() { 51 | return this._extractTopicSignature(this.parentTopic); 52 | } 53 | 54 | get childPropertySignature() { 55 | return this._extractTopicSignature(this.childTopic); 56 | } 57 | 58 | _extractTopicSignature(topic) { 59 | switch (typeof topic) { 60 | case 'function': 61 | return this._extractFunctionSignature(topic); 62 | default: 63 | return this.key; 64 | } 65 | } 66 | 67 | _extractFunctionSignature(fn) { 68 | return fn 69 | .toString() 70 | .replace( 71 | FUNCTION_REGEXP, 72 | (match, name = this.key, params) => name + params 73 | ); 74 | } 75 | 76 | constructor(parentKlass, childKlass, parentDescriptor, childDescriptor) { 77 | this.parentKlass = parentKlass; 78 | this.childKlass = childKlass; 79 | this.parentDescriptor = parentDescriptor; 80 | this.childDescriptor = childDescriptor; 81 | } 82 | 83 | assert(condition, msg = '') { 84 | if (condition !== true) { 85 | this.error(GENERIC_FUNCTION_ERROR + msg); 86 | } 87 | } 88 | 89 | error(msg) { 90 | msg = msg 91 | // Replace lazily, because they actually might not 92 | // be available in all cases 93 | .replace('{parent}', m => this.parentNotation) 94 | .replace('{child}', m => this.childNotation); 95 | throw new SyntaxError(msg); 96 | } 97 | } 98 | 99 | function getDescriptorType(descriptor) { 100 | if (descriptor.hasOwnProperty('value')) { 101 | return 'data'; 102 | } 103 | 104 | if (descriptor.hasOwnProperty('get') || descriptor.hasOwnProperty('set')) { 105 | return 'accessor'; 106 | } 107 | 108 | // If none of them exist, browsers treat it as 109 | // a data descriptor with a value of `undefined` 110 | return 'data'; 111 | } 112 | 113 | function checkFunctionSignatures(parent, child, reporter) { 114 | reporter.assert(parent.length === child.length); 115 | } 116 | 117 | function checkDataDescriptors(parent, child, reporter) { 118 | const parentValueType = typeof parent.value; 119 | const childValueType = typeof child.value; 120 | 121 | if (parentValueType === 'undefined' && childValueType === 'undefined') { 122 | // class properties can be any expression, which isn't ran until the 123 | // the instance is created, so we can't reliably get type information 124 | // for them yet (per spec). Perhaps when Babel includes flow-type info 125 | // in runtime? Tried regex solutions, but super hacky and only feasible 126 | // on primitives, which is confusing for usage... 127 | reporter.error(`descriptor values are both undefined. (class properties are are not currently supported)'`); 128 | } 129 | 130 | if (parentValueType !== childValueType) { 131 | const isFunctionOverUndefined = (childValueType === 'function' && parentValueType === undefined); 132 | // Even though we don't support class properties, this 133 | // will still handle more than just functions, just in case. 134 | // Shadowing an undefined value is an error if the inherited 135 | // value was undefined (usually a class property, not a method) 136 | if (isFunctionOverUndefined || parentValueType !== undefined) { 137 | reporter.error(`value types do not match. {parent} is "${parentValueType}", {child} is "${childValueType}"`); 138 | } 139 | } 140 | 141 | // Switch, in preparation for supporting more types 142 | switch (childValueType) { 143 | case 'function': 144 | checkFunctionSignatures(parent.value, child.value, reporter); 145 | break; 146 | 147 | default: 148 | reporter.error(`Unexpected error. Please file a bug with: {parent} is "${parentValueType}", {child} is "${childValueType}"`); 149 | break; 150 | } 151 | } 152 | 153 | function checkAccessorDescriptors(parent, child, reporter) { 154 | const parentHasGetter = typeof parent.get === 'function'; 155 | const childHasGetter = typeof child.get === 'function'; 156 | const parentHasSetter = typeof parent.set === 'function'; 157 | const childHasSetter = typeof child.set === 'function'; 158 | 159 | if (parentHasGetter || childHasGetter) { 160 | if (!parentHasGetter && parentHasSetter) { 161 | reporter.error(`{parent} is setter but {child} is getter`); 162 | } 163 | 164 | if (!childHasGetter && childHasSetter) { 165 | reporter.error(`{parent} is getter but {child} is setter`); 166 | } 167 | 168 | checkFunctionSignatures(parent.get, child.get, reporter); 169 | } 170 | 171 | if (parentHasSetter || childHasSetter) { 172 | if (!parentHasSetter && parentHasGetter) { 173 | reporter.error(`{parent} is getter but {child} is setter`); 174 | } 175 | 176 | if (!childHasSetter && childHasGetter) { 177 | reporter.error(`{parent} is setter but {child} is getter`); 178 | } 179 | 180 | checkFunctionSignatures(parent.set, child.set, reporter); 181 | } 182 | } 183 | 184 | function checkDescriptors(parent, child, reporter) { 185 | const parentType = getDescriptorType(parent); 186 | const childType = getDescriptorType(child); 187 | 188 | if (parentType !== childType) { 189 | reporter.error(`descriptor types do not match. {parent} is "${parentType}", {child} is "${childType}"`); 190 | } 191 | 192 | switch (childType) { 193 | case 'data': 194 | checkDataDescriptors(parent, child, reporter); 195 | break; 196 | 197 | case 'accessor': 198 | checkAccessorDescriptors(parent, child, reporter); 199 | break; 200 | } 201 | } 202 | 203 | const suggestionTransforms = [ 204 | key => key.toLowerCase(), 205 | key => key.toUpperCase(), 206 | key => key + 's', 207 | key => key.slice(0, -1), 208 | key => key.slice(1, key.length), 209 | ]; 210 | 211 | function findPossibleAlternatives(superKlass, key) { 212 | for (let i = 0, l = suggestionTransforms.length; i < l; i++) { 213 | const fn = suggestionTransforms[i]; 214 | const suggestion = fn(key); 215 | 216 | if (suggestion in superKlass) { 217 | return suggestion; 218 | } 219 | } 220 | 221 | return null; 222 | } 223 | 224 | function handleDescriptor(target, key, descriptor) { 225 | descriptor.key = key; 226 | const superKlass = Object.getPrototypeOf(target); 227 | const superDescriptor = Object.getOwnPropertyDescriptor(superKlass, key); 228 | const reporter = new SyntaxErrorReporter(superKlass, target, superDescriptor, descriptor); 229 | 230 | if (superDescriptor === undefined) { 231 | const suggestedKey = findPossibleAlternatives(superKlass, key); 232 | const suggestion = suggestedKey ? `\n\n Did you mean "${suggestedKey}"?` : ''; 233 | reporter.error(`No descriptor matching {child} was found on the prototype chain.${suggestion}`); 234 | } 235 | 236 | checkDescriptors(superDescriptor, descriptor, reporter); 237 | 238 | return descriptor; 239 | } 240 | 241 | export default function override(...args) { 242 | return decorate(handleDescriptor, args); 243 | } 244 | -------------------------------------------------------------------------------- /src/private/utils.js: -------------------------------------------------------------------------------- 1 | import lazyInitialize from '../lazy-initialize'; 2 | 3 | const { defineProperty, getOwnPropertyDescriptor, 4 | getOwnPropertyNames, getOwnPropertySymbols } = Object; 5 | 6 | export function isDescriptor(desc) { 7 | if (!desc || !desc.hasOwnProperty) { 8 | return false; 9 | } 10 | 11 | const keys = ['value', 'initializer', 'get', 'set']; 12 | 13 | for (let i = 0, l = keys.length; i < l; i++) { 14 | if (desc.hasOwnProperty(keys[i])) { 15 | return true; 16 | } 17 | } 18 | 19 | return false; 20 | } 21 | 22 | export function decorate(handleDescriptor, entryArgs) { 23 | if (isDescriptor(entryArgs[entryArgs.length - 1])) { 24 | return handleDescriptor(...entryArgs, []); 25 | } else { 26 | return function () { 27 | return handleDescriptor(...Array.prototype.slice.call(arguments), entryArgs); 28 | }; 29 | } 30 | } 31 | 32 | class Meta { 33 | @lazyInitialize 34 | debounceTimeoutIds = {}; 35 | 36 | @lazyInitialize 37 | throttleTimeoutIds = {}; 38 | 39 | @lazyInitialize 40 | throttlePreviousTimestamps = {}; 41 | 42 | @lazyInitialize 43 | throttleTrailingArgs = null; 44 | 45 | @lazyInitialize 46 | profileLastRan = null; 47 | } 48 | 49 | const META_KEY = (typeof Symbol === 'function') 50 | ? Symbol('__core_decorators__') 51 | : '__core_decorators__'; 52 | 53 | export function metaFor(obj) { 54 | if (obj.hasOwnProperty(META_KEY) === false) { 55 | defineProperty(obj, META_KEY, { 56 | // Defaults: NOT enumerable, configurable, or writable 57 | value: new Meta() 58 | }); 59 | } 60 | 61 | return obj[META_KEY]; 62 | } 63 | 64 | export const getOwnKeys = getOwnPropertySymbols 65 | ? function (object) { 66 | return getOwnPropertyNames(object) 67 | .concat(getOwnPropertySymbols(object)); 68 | } 69 | : getOwnPropertyNames; 70 | 71 | 72 | export function getOwnPropertyDescriptors(obj) { 73 | const descs = {}; 74 | 75 | getOwnKeys(obj).forEach( 76 | key => (descs[key] = getOwnPropertyDescriptor(obj, key)) 77 | ); 78 | 79 | return descs; 80 | } 81 | 82 | export function createDefaultSetter(key) { 83 | return function set(newValue) { 84 | Object.defineProperty(this, key, { 85 | configurable: true, 86 | writable: true, 87 | // IS enumerable when reassigned by the outside word 88 | enumerable: true, 89 | value: newValue 90 | }); 91 | 92 | return newValue; 93 | }; 94 | } 95 | 96 | export function bind(fn, context) { 97 | if (fn.bind) { 98 | return fn.bind(context); 99 | } else { 100 | return function __autobind__() { 101 | return fn.apply(context, arguments); 102 | }; 103 | } 104 | } 105 | 106 | export const warn = (() => { 107 | if (typeof console !== 'object' || !console || typeof console.warn !== 'function') { 108 | return () => {}; 109 | } else { 110 | return bind(console.warn, console); 111 | } 112 | })(); 113 | 114 | const seenDeprecations = {}; 115 | export function internalDeprecation(msg) { 116 | if (seenDeprecations[msg] !== true) { 117 | seenDeprecations[msg] = true; 118 | warn('DEPRECATION: ' + msg); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/profile.js: -------------------------------------------------------------------------------- 1 | import { decorate, metaFor, warn, bind } from './private/utils'; 2 | 3 | const oc = console; 4 | 5 | // Exported for mocking in tests 6 | export const defaultConsole = { 7 | profile: console.profile ? bind(console.profile, console) : () => {}, 8 | profileEnd: console.profileEnd ? bind(console.profileEnd, console) : () => {}, 9 | warn 10 | }; 11 | 12 | function handleDescriptor(target, key, descriptor, [prefix = null, onceThrottleOrFunction = false, console = defaultConsole]) { 13 | if (!profile.__enabled) { 14 | if (!profile.__warned) { 15 | console.warn('console.profile is not supported. All @profile decorators are disabled.'); 16 | profile.__warned = true; 17 | } 18 | return descriptor; 19 | } 20 | 21 | const fn = descriptor.value; 22 | 23 | if (prefix === null) { 24 | prefix = `${target.constructor.name}.${key}`; 25 | } 26 | 27 | if (typeof fn !== 'function') { 28 | throw new SyntaxError(`@profile can only be used on functions, not: ${fn}`); 29 | } 30 | 31 | return { 32 | ...descriptor, 33 | value() { 34 | const now = Date.now(); 35 | const meta = metaFor(this); 36 | if ( 37 | (onceThrottleOrFunction === true && !meta.profileLastRan) || 38 | (onceThrottleOrFunction === false) || 39 | (typeof onceThrottleOrFunction === 'number' && (now - meta.profileLastRan) > onceThrottleOrFunction) || 40 | (typeof onceThrottleOrFunction === 'function' && onceThrottleOrFunction.apply(this, arguments)) 41 | ) { 42 | console.profile(prefix); 43 | meta.profileLastRan = now; 44 | } 45 | 46 | try { 47 | return fn.apply(this, arguments); 48 | } finally { 49 | console.profileEnd(prefix); 50 | } 51 | } 52 | } 53 | } 54 | 55 | export default function profile(...args) { 56 | return decorate(handleDescriptor, args); 57 | } 58 | 59 | // Only Chrome, Firefox, and Edge support profile. 60 | // Exposing properties for testing. 61 | profile.__enabled = !!console.profile; 62 | profile.__warned = false; 63 | -------------------------------------------------------------------------------- /src/readonly.js: -------------------------------------------------------------------------------- 1 | import { decorate } from './private/utils'; 2 | 3 | function handleDescriptor(target, key, descriptor) { 4 | descriptor.writable = false; 5 | return descriptor; 6 | } 7 | 8 | export default function readonly(...args) { 9 | return decorate(handleDescriptor, args); 10 | } 11 | -------------------------------------------------------------------------------- /src/suppress-warnings.js: -------------------------------------------------------------------------------- 1 | import { decorate } from './private/utils'; 2 | 3 | function suppressedWarningNoop() { 4 | // Warnings are currently suppressed via @suppressWarnings 5 | } 6 | 7 | function applyWithoutWarnings(context, fn, args) { 8 | if (typeof console === 'object') { 9 | const nativeWarn = console.warn; 10 | console.warn = suppressedWarningNoop; 11 | const ret = fn.apply(context, args); 12 | console.warn = nativeWarn; 13 | return ret; 14 | } else { 15 | return fn.apply(context, args); 16 | } 17 | } 18 | 19 | function handleDescriptor(target, key, descriptor) { 20 | return { 21 | ...descriptor, 22 | value: function suppressWarningsWrapper() { 23 | return applyWithoutWarnings(this, descriptor.value, arguments); 24 | } 25 | }; 26 | } 27 | 28 | export default function suppressWarnings(...args) { 29 | return decorate(handleDescriptor, args); 30 | } 31 | -------------------------------------------------------------------------------- /src/throttle.js: -------------------------------------------------------------------------------- 1 | import { decorate, metaFor, internalDeprecation } from './private/utils'; 2 | 3 | const DEFAULT_TIMEOUT = 300; 4 | 5 | function handleDescriptor(target, key, descriptor, [wait = DEFAULT_TIMEOUT, options = {}]) { 6 | const callback = descriptor.value; 7 | 8 | if (typeof callback !== 'function') { 9 | throw new SyntaxError('Only functions can be throttled'); 10 | } 11 | 12 | if (options.leading !== false) { 13 | options.leading = true 14 | } 15 | 16 | if(options.trailing !== false) { 17 | options.trailing = true 18 | } 19 | 20 | return { 21 | ...descriptor, 22 | value() { 23 | const meta = metaFor(this); 24 | const { throttleTimeoutIds, throttlePreviousTimestamps } = meta; 25 | const timeout = throttleTimeoutIds[key]; 26 | // last execute timestamp 27 | let previous = throttlePreviousTimestamps[key] || 0; 28 | const now = Date.now(); 29 | 30 | if (options.trailing) { 31 | meta.throttleTrailingArgs = arguments; 32 | } 33 | 34 | // if first be called and disable the execution on the leading edge 35 | // set last execute timestamp to now 36 | if (!previous && options.leading === false) { 37 | previous = now; 38 | } 39 | 40 | const remaining = wait - (now - previous); 41 | 42 | if (remaining <= 0) { 43 | clearTimeout(timeout); 44 | delete throttleTimeoutIds[key]; 45 | throttlePreviousTimestamps[key] = now; 46 | callback.apply(this, arguments); 47 | } else if (!timeout && options.trailing) { 48 | throttleTimeoutIds[key] = setTimeout(() => { 49 | throttlePreviousTimestamps[key] = options.leading === false ? 0 : Date.now(); 50 | delete throttleTimeoutIds[key]; 51 | callback.apply(this, meta.throttleTrailingArgs); 52 | // don't leak memory! 53 | meta.throttleTrailingArgs = null; 54 | }, remaining); 55 | } 56 | } 57 | }; 58 | } 59 | 60 | export default function throttle(...args) { 61 | internalDeprecation('@throttle is deprecated and will be removed shortly. Use @throttle from lodash-decorators.\n\n https://www.npmjs.com/package/lodash-decorators'); 62 | return decorate(handleDescriptor, args); 63 | } 64 | -------------------------------------------------------------------------------- /src/time.js: -------------------------------------------------------------------------------- 1 | import { decorate } from './private/utils'; 2 | 3 | const labels = {}; 4 | 5 | // Exported for mocking in tests 6 | export const defaultConsole = { 7 | time: console.time ? console.time.bind(console) : label => { 8 | labels[label] = new Date(); 9 | }, 10 | timeEnd: console.timeEnd ? console.timeEnd.bind(console) : label => { 11 | const timeNow = new Date(); 12 | const timeTaken = timeNow - labels[label]; 13 | delete labels[label]; 14 | console.log(`${label}: ${timeTaken}ms`); 15 | } 16 | }; 17 | 18 | let count = 0; 19 | 20 | function handleDescriptor(target, key, descriptor, [prefix = null, console = defaultConsole]) { 21 | const fn = descriptor.value; 22 | 23 | if (prefix === null) { 24 | prefix = `${target.constructor.name}.${key}`; 25 | } 26 | 27 | if (typeof fn !== 'function') { 28 | throw new SyntaxError(`@time can only be used on functions, not: ${fn}`); 29 | } 30 | 31 | return { 32 | ...descriptor, 33 | value() { 34 | const label = `${prefix}-${count}`; 35 | count++; 36 | console.time(label); 37 | 38 | try { 39 | return fn.apply(this, arguments); 40 | } finally { 41 | console.timeEnd(label); 42 | } 43 | } 44 | } 45 | } 46 | 47 | export default function time(...args) { 48 | return decorate(handleDescriptor, args); 49 | } 50 | -------------------------------------------------------------------------------- /test/exports.spec.js: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai' 2 | import * as path from 'path'; 3 | import * as glob from 'glob'; 4 | import toCamelCase from 'camelcase'; 5 | import interopRequire from 'interop-require'; 6 | import * as decorators from '../'; 7 | 8 | const should = chai.should(); 9 | 10 | const aliases = { 11 | deprecate: 'deprecated', 12 | mixin: 'mixins' 13 | }; 14 | 15 | describe('Main package exports', function () { 16 | const libPath = path.normalize(`${__dirname}/../lib`); 17 | let filePaths; 18 | 19 | beforeEach(function () { 20 | filePaths = glob.sync(`${libPath}/*.js`, { 21 | ignore: ['**/core-decorators.js', '**/*.spec.js'] 22 | }); 23 | }); 24 | 25 | afterEach(function () { 26 | filePaths = null; 27 | }); 28 | 29 | it('exports all defined decorator files inside core-decorators.js', function () { 30 | const aliasKeys = Object.keys(aliases); 31 | const unseen = Object.keys(decorators); 32 | 33 | function markAsSeen(name) { 34 | unseen.splice(unseen.indexOf(name), 1); 35 | } 36 | 37 | filePaths.forEach(filePath => { 38 | const name = toCamelCase( 39 | path.basename(filePath, '.js') 40 | ); 41 | const decorator = interopRequire(filePath); 42 | should.exist(decorators[name], `@${name} should be exported`); 43 | decorators[name].should.equal(decorator, `export @${name} is the expected function`); 44 | 45 | markAsSeen(name); 46 | if (aliasKeys.indexOf(name) !== -1) { 47 | markAsSeen(aliases[name]); 48 | } 49 | }); 50 | 51 | unseen.should.deep.equal([], 'all exports were seen'); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/test.spec.js: -------------------------------------------------------------------------------- 1 | import * as chai from 'chai'; 2 | import sinon from 'sinon'; 3 | import sinonChai from 'sinon-chai'; 4 | 5 | chai.should(); 6 | chai.use(sinonChai); 7 | -------------------------------------------------------------------------------- /test/unit/applyDecorators.spec.js: -------------------------------------------------------------------------------- 1 | import applyDecorators from '../../lib/applyDecorators'; 2 | import autobind from '../../lib/autobind'; 3 | import readonly from '../../lib/readonly'; 4 | import enumerable from '../../lib/enumerable'; 5 | 6 | describe('applyDecorators() helper', function () { 7 | class Foo { 8 | first() { 9 | return this; 10 | } 11 | 12 | second() { 13 | return this; 14 | } 15 | } 16 | 17 | applyDecorators(Foo, { 18 | first: [autobind], 19 | second: [readonly, enumerable] 20 | }) 21 | 22 | it('applies the decorators to the provided prop\'s descriptors', function () { 23 | const foo = new Foo(); 24 | 25 | const { first } = foo; 26 | first().should.equal(foo); 27 | foo.second().should.equal(foo); 28 | 29 | (function () { 30 | foo.second = 'I will error'; 31 | }).should.throw('Cannot assign to read only property \'second\' of object \'#\''); 32 | 33 | Object.getOwnPropertyDescriptor(Foo.prototype, 'second') 34 | .writable.should.equal(false); 35 | 36 | Object.getOwnPropertyDescriptor(Foo.prototype, 'second') 37 | .enumerable.should.equal(true); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/unit/autobind.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import autobind from '../../lib/autobind'; 3 | 4 | const root = (typeof window !== 'undefined') ? window : global; 5 | 6 | describe('@autobind', function () { 7 | let Foo; 8 | let Bar; 9 | let Car; 10 | let barCount; 11 | 12 | beforeEach(function () { 13 | Foo = class Foo { 14 | @autobind 15 | getFoo() { 16 | return this; 17 | } 18 | 19 | getFooAgain() { 20 | return this; 21 | } 22 | 23 | @autobind 24 | onlyOnFoo() { 25 | return this; 26 | } 27 | } 28 | 29 | barCount = 0; 30 | 31 | Bar = class Bar extends Foo { 32 | @autobind() 33 | getFoo() { 34 | const foo = super.getFoo(); 35 | barCount++; 36 | return foo; 37 | } 38 | 39 | getSuperMethod_getFoo() { 40 | return super.getFoo; 41 | } 42 | 43 | getSuperMethod_onlyOnFoo() { 44 | return super.onlyOnFoo; 45 | } 46 | 47 | @autobind 48 | onlyOnBar() { 49 | return this; 50 | } 51 | } 52 | 53 | Car = class Car extends Foo { 54 | @autobind() 55 | getCarFromFoo() { 56 | return super.onlyOnFoo(); 57 | } 58 | } 59 | }); 60 | 61 | afterEach(function () { 62 | Foo = null; 63 | Bar = null; 64 | barCount = null; 65 | }); 66 | 67 | it('returns a bound instance for a method', function () { 68 | const foo = new Foo(); 69 | const { getFoo } = foo; 70 | 71 | getFoo().should.equal(foo); 72 | 73 | }); 74 | 75 | it('sets the correct prototype descriptor options', function () { 76 | const desc = Object.getOwnPropertyDescriptor(Foo.prototype, 'getFoo'); 77 | 78 | desc.configurable.should.equal(true); 79 | desc.enumerable.should.equal(false); 80 | }); 81 | 82 | it('sets the correct instance descriptor options when bound', function () { 83 | const foo = new Foo(); 84 | const { getFoo } = foo; 85 | const desc = Object.getOwnPropertyDescriptor(foo, 'getFoo'); 86 | 87 | desc.configurable.should.equal(true); 88 | desc.enumerable.should.equal(false); 89 | desc.writable.should.equal(true); 90 | desc.value.should.equal(getFoo); 91 | }); 92 | 93 | it('sets the correct instance descriptor options when reassigned outside', function () { 94 | const noop = function () {}; 95 | const foo = new Foo(); 96 | const ret = foo.getFoo = noop; 97 | const desc = Object.getOwnPropertyDescriptor(foo, 'getFoo'); 98 | 99 | ret.should.equal(noop); 100 | desc.configurable.should.equal(true); 101 | desc.enumerable.should.equal(true); 102 | desc.writable.should.equal(true); 103 | desc.value.should.equal(noop); 104 | }); 105 | 106 | it('works with multiple instances of the same class', function () { 107 | const foo1 = new Foo(); 108 | const foo2 = new Foo(); 109 | 110 | const getFoo1 = foo1.getFoo; 111 | const getFoo2 = foo2.getFoo; 112 | 113 | getFoo1().should.equal(foo1); 114 | getFoo2().should.equal(foo2); 115 | }); 116 | 117 | it('returns the same bound function every time', function () { 118 | const foo = new Foo(); 119 | const bar = new Bar(); 120 | 121 | foo.getFoo.should.equal(foo.getFoo); 122 | bar.getFoo.should.equal(bar.getFoo); 123 | bar.getSuperMethod_getFoo().should.equal(bar.getSuperMethod_getFoo()); 124 | bar.getFooAgain().should.equal(bar.getFooAgain()); 125 | }); 126 | 127 | it('works with inheritance, super.method() being autobound as well', function () { 128 | const bar = new Bar(); 129 | const car = new Car(); 130 | 131 | const getFooFromBar = bar.getFoo; 132 | const getCarFromFoo = car.getCarFromFoo; 133 | 134 | // Calling both forms more than once to catch 135 | // bugs that only appear after first invocation 136 | getFooFromBar().should.equal(bar); 137 | getFooFromBar().should.equal(bar); 138 | getCarFromFoo().should.equal(car); 139 | getCarFromFoo().should.equal(car); 140 | 141 | bar.getFoo().should.equal(bar); 142 | bar.getFoo().should.equal(bar); 143 | bar.getFooAgain().should.equal(bar); 144 | const getSuperMethod_getFoo = bar.getSuperMethod_getFoo(); 145 | getSuperMethod_getFoo().should.equal(bar); 146 | const onlyOnFoo = bar.getSuperMethod_onlyOnFoo(); 147 | onlyOnFoo().should.equal(bar); 148 | 149 | 150 | barCount.should.equal(4); 151 | }); 152 | 153 | it('throws when it needs WeakMap but it is not available', function () { 154 | const WeakMap = root.WeakMap; 155 | delete root.WeakMap; 156 | 157 | const bar = new Bar(); 158 | 159 | (function () { 160 | bar.getFoo(); 161 | }).should.throw(`Using @autobind on getFoo() requires WeakMap support due to its use of super.getFoo() 162 | See https://github.com/jayphelps/core-decorators.js/issues/20`); 163 | 164 | barCount.should.equal(0); 165 | 166 | root.WeakMap = WeakMap; 167 | }); 168 | 169 | it('does not override descriptor when accessed on the prototype', function () { 170 | Bar.prototype.getFoo; 171 | Bar.prototype.onlyOnBar; 172 | 173 | const bar = new Bar(); 174 | const getFoo2 = bar.getFoo; 175 | const onlyOnBar = bar.onlyOnBar; 176 | getFoo2().should.equal(bar); 177 | onlyOnBar().should.equal(bar); 178 | 179 | barCount.should.equal(1); 180 | 181 | // check Foo after Bar since it was inherited by Bar and might accidentally 182 | // be bound to the instance of Foo above! 183 | Foo.prototype.getFoo; 184 | Foo.prototype.onlyOnFoo; 185 | 186 | const foo = new Foo(); 187 | const getFoo1 = foo.getFoo; 188 | const onlyOnFoo = foo.onlyOnFoo; 189 | getFoo1().should.equal(foo); 190 | onlyOnFoo().should.equal(foo); 191 | 192 | }); 193 | 194 | it('can be used to autobind the entire class at once', function () { 195 | // do not @autobind, which means start() should return `undefined` if 196 | // it is detached from the instance 197 | class Vehicle { 198 | start() { 199 | return this; 200 | } 201 | } 202 | 203 | @autobind 204 | class Car extends Vehicle { 205 | constructor() { 206 | super(); 207 | this.name = 'amazing'; 208 | } 209 | 210 | get color() { 211 | return 'red'; 212 | } 213 | 214 | drive() { 215 | return this; 216 | } 217 | 218 | stop() { 219 | return this; 220 | } 221 | 222 | render() { 223 | return this; 224 | } 225 | } 226 | 227 | const originalRender = Car.prototype.render; 228 | Car.prototype.render = function () { 229 | return originalRender.apply(this, arguments); 230 | }; 231 | 232 | Car.prototype.stop; 233 | Car.prototype.color; 234 | 235 | const car = new Car(); 236 | car.render().should.equal(car); 237 | const { drive, stop, name, color, start } = car; 238 | expect(drive()).to.equal(car); 239 | drive().should.equal(car); 240 | stop().should.equal(car); 241 | name.should.equal('amazing'); 242 | color.should.equal('red'); 243 | 244 | // We didn't @autobind Vehicle 245 | expect(start()).to.be.undefined; 246 | }); 247 | 248 | it('correctly binds with multiple class prototype levels', function () { 249 | @autobind 250 | class A { 251 | method() { 252 | return this.test || 'WRONG ONE'; 253 | } 254 | } 255 | 256 | @autobind 257 | class B extends A {} 258 | 259 | @autobind 260 | class C extends B { 261 | test = 'hello'; 262 | 263 | method() { 264 | return super.method(); 265 | } 266 | } 267 | 268 | const c = new C(); 269 | const { method } = c; 270 | method().should.equal('hello'); 271 | 272 | const method2 = A.prototype.method; 273 | method2.call({ test: 'first' }).should.equal('first'); 274 | 275 | const method3 = B.prototype.method; 276 | method3.call({ test: 'second' }).should.equal('second'); 277 | }); 278 | 279 | it('correctly binds class with symbol properties', function () { 280 | const parkHash = Symbol('park'); 281 | 282 | @autobind 283 | class Car { 284 | [parkHash]() { 285 | return this; 286 | } 287 | } 288 | 289 | const car = new Car(); 290 | const park = car[parkHash]; 291 | park().should.equal(car); 292 | }) 293 | }); 294 | -------------------------------------------------------------------------------- /test/unit/debounce.spec.js: -------------------------------------------------------------------------------- 1 | import { useFakeTimers } from 'sinon'; 2 | import debounce from '../../lib/debounce'; 3 | 4 | class Editor { 5 | counter = 0; 6 | 7 | @debounce(500) 8 | updateCounter1() { 9 | this.counter++; 10 | } 11 | 12 | @debounce(500, true) 13 | updateCounter2() { 14 | this.counter++; 15 | } 16 | } 17 | 18 | describe('@debounce', function () { 19 | let editor; 20 | let clock; 21 | 22 | beforeEach(function () { 23 | editor = new Editor(); 24 | clock = useFakeTimers(Date.now()); 25 | }); 26 | 27 | afterEach(function () { 28 | clock.restore(); 29 | }); 30 | 31 | it('invokes function only once', function () { 32 | editor.updateCounter1(); 33 | editor.counter.should.equal(0); 34 | 35 | clock.tick(600); 36 | 37 | editor.counter.should.equal(1); 38 | }); 39 | 40 | it('invokes function immediately and only once if "immediate" option is true', function () { 41 | editor.updateCounter2(); 42 | editor.counter.should.equal(1); 43 | 44 | clock.tick(400); 45 | 46 | editor.counter.should.equal(1); 47 | 48 | clock.tick(200); 49 | 50 | editor.counter.should.equal(1); 51 | }); 52 | 53 | it('does not share timers between instances', function () { 54 | let editor2 = new Editor(); 55 | editor.updateCounter1(); 56 | editor2.updateCounter1(); 57 | 58 | clock.tick(600); 59 | 60 | editor.counter.should.equal(1); 61 | editor2.counter.should.equal(1); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/unit/decorate.spec.js: -------------------------------------------------------------------------------- 1 | import decorate from '../../lib/decorate'; 2 | import { memoize } from 'lodash'; 3 | 4 | describe('@decorate', function () { 5 | let Foo; 6 | let callCount; 7 | 8 | beforeEach(function () { 9 | const append = function (fn, suffix) { 10 | return function (msg) { 11 | return fn.call(this, msg) + suffix; 12 | }; 13 | }; 14 | 15 | callCount = 0; 16 | 17 | Foo = class Foo { 18 | @decorate(append, '!') 19 | suchWow(something) { 20 | return something + 'bro'; 21 | } 22 | 23 | @decorate(append, '!') 24 | @decorate(append, '!') 25 | suchWowTwice(something) { 26 | return something + 'bro'; 27 | } 28 | 29 | @decorate(append, '!') 30 | @decorate(append, '!') 31 | @decorate(append, '!') 32 | suchWowThrice(something) { 33 | return something + 'bro'; 34 | } 35 | 36 | @decorate(memoize) 37 | getFoo() { 38 | callCount++; 39 | return this; 40 | } 41 | } 42 | }); 43 | 44 | it('correctly applies user provided function to method', function () { 45 | const foo = new Foo(); 46 | 47 | foo.suchWow('dude').should.equal('dudebro!'); 48 | foo.suchWowTwice('dude').should.equal('dudebro!!'); 49 | foo.suchWowThrice('dude').should.equal('dudebro!!!'); 50 | }); 51 | 52 | it('sets the correct prototype descriptor options', function () { 53 | const desc = Object.getOwnPropertyDescriptor(Foo.prototype, 'suchWow'); 54 | 55 | desc.configurable.should.equal(true); 56 | desc.enumerable.should.equal(false); 57 | }); 58 | 59 | it('is tied to the instance, not the prototype', function () { 60 | const foo1 = new Foo(); 61 | const foo2 = new Foo(); 62 | 63 | const foo1Result = foo1.getFoo(); 64 | const foo2Result = foo2.getFoo(); 65 | 66 | foo1Result.should.not.equal(foo2Result); 67 | foo1Result.should.equal(foo1); 68 | foo2Result.should.equal(foo2); 69 | 70 | foo1.getFoo(); 71 | 72 | callCount.should.equal(2); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /test/unit/deprecate.spec.js: -------------------------------------------------------------------------------- 1 | import sinon from 'sinon'; 2 | import deprecate from '../../lib/deprecate'; 3 | import * as utils from '../../lib/private/utils'; 4 | 5 | class Foo { 6 | @deprecate 7 | first() { 8 | return 'hello world'; 9 | } 10 | 11 | second() { 12 | return this.first(); 13 | } 14 | 15 | @deprecate('asdf') 16 | third() { 17 | return 'hello galaxy'; 18 | } 19 | 20 | @deprecate('fdsa', { url: 'http://example.com/' }) 21 | forth() { 22 | return 'hello universe'; 23 | } 24 | } 25 | 26 | describe('@deprecate', function () { 27 | beforeEach(function () { 28 | sinon.spy(utils, 'warn'); 29 | }); 30 | 31 | afterEach(function () { 32 | utils.warn.restore(); 33 | }); 34 | 35 | it('console.warn() is called with default warning when the deprecated function is used', function () { 36 | const foo = new Foo(); 37 | 38 | foo.first().should.equal('hello world'); 39 | utils.warn.should.have.been.calledOnce; 40 | utils.warn.should.have.been.calledWith('DEPRECATION Foo#first: This function will be removed in future versions.'); 41 | 42 | foo.second().should.equal('hello world'); 43 | utils.warn.should.have.been.calledTwice; 44 | utils.warn.should.have.been.calledWith('DEPRECATION Foo#first: This function will be removed in future versions.'); 45 | }); 46 | 47 | it('console.warn() is called with the custom message, when provided', function () { 48 | return; 49 | const foo = new Foo(); 50 | 51 | foo.third().should.equal('hello galaxy'); 52 | utils.warn.should.have.been.calledOnce; 53 | utils.warn.should.have.been.calledWith('asdf'); 54 | }); 55 | 56 | it('console.warn() is called with the URL, when provided', function () { 57 | const foo = new Foo(); 58 | 59 | foo.forth().should.equal('hello universe'); 60 | utils.warn.should.have.been.calledOnce; 61 | utils.warn.should.have.been.calledWith('DEPRECATION Foo#forth: fdsa\n\n See http://example.com/ for more details.\n\n'); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /test/unit/enumerable.spec.js: -------------------------------------------------------------------------------- 1 | import enumerable from '../../lib/enumerable'; 2 | 3 | describe('@enumerable', function () { 4 | class Foo { 5 | @enumerable 6 | bar(){} 7 | } 8 | 9 | it('is enumerable', function () { 10 | Object.getOwnPropertyDescriptor(Foo.prototype, 'bar') 11 | .enumerable.should.equal(true); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/unit/extendDescriptor.js: -------------------------------------------------------------------------------- 1 | import extendDescriptor from '../../lib/extendDescriptor'; 2 | import enumerable from '../../lib/enumerable'; 3 | import nonenumerable from '../../lib/nonenumerable'; 4 | 5 | describe('@extendDescriptor', function () { 6 | class Base { 7 | get first() { 8 | return this._first; 9 | } 10 | 11 | set second(value) { 12 | this._second = value; 13 | } 14 | 15 | set third(value) { 16 | throw new Error('should not be called'); 17 | } 18 | 19 | @nonenumerable 20 | fourth = 'fourth'; 21 | 22 | @enumerable 23 | fifth() { 24 | throw new Error('should not be called'); 25 | } 26 | } 27 | 28 | class Middle extends Base {} 29 | 30 | class Derived extends Middle { 31 | @extendDescriptor 32 | set first(value) { 33 | this._first = value; 34 | } 35 | 36 | @extendDescriptor 37 | get second() { 38 | return this._second; 39 | } 40 | 41 | @extendDescriptor 42 | get third() { 43 | return this._third; 44 | } 45 | 46 | set third(value) { 47 | this._third = value; 48 | } 49 | 50 | @extendDescriptor 51 | fourth = 'fourth'; 52 | 53 | @extendDescriptor 54 | fifth() { 55 | return 'fifth'; 56 | } 57 | } 58 | 59 | let derived; 60 | 61 | beforeEach(() => { 62 | derived = new Derived(); 63 | }); 64 | 65 | afterEach(() => { 66 | derived = null; 67 | }); 68 | 69 | it('extends getters/setters', function () { 70 | derived.first = 'first'; 71 | derived.first.should.equal('first'); 72 | 73 | derived.second = 'second'; 74 | derived.second.should.equal('second'); 75 | 76 | derived.third = 'third'; 77 | derived.third.should.equal('third'); 78 | }); 79 | 80 | it('extends property initializers', function () { 81 | const descriptor = Object.getOwnPropertyDescriptor(Derived.prototype, 'fourth'); 82 | descriptor.enumerable.should.equal(false); 83 | 84 | derived.fourth.should.equal('fourth'); 85 | }); 86 | 87 | it('extends property methods', function () { 88 | const descriptor = Object.getOwnPropertyDescriptor(Derived.prototype, 'fifth'); 89 | descriptor.enumerable.should.equal(true); 90 | 91 | derived.fifth().should.equal('fourth'); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /test/unit/lazy-initialize.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { spy } from 'sinon'; 3 | import lazyInitialize from '../../lib/lazy-initialize'; 4 | 5 | describe('@lazyInitialize', function () { 6 | let initializer; 7 | 8 | class Foo { 9 | @lazyInitialize 10 | bar = initializer(); 11 | } 12 | 13 | beforeEach(function () { 14 | initializer = spy(() => 'test'); 15 | }); 16 | 17 | afterEach(function () { 18 | initializer = null; 19 | }); 20 | 21 | it('does not initialize property until it the getter is called', function () { 22 | const foo = new Foo(); 23 | initializer.should.not.have.been.called; 24 | foo.bar.should.equal('test'); 25 | foo.bar.should.equal('test'); 26 | initializer.should.have.been.called.once; 27 | }); 28 | 29 | it('allows normal reassignment', function () { 30 | const foo = new Foo(); 31 | foo.bar = 'test'; 32 | initializer.should.not.have.been.called; 33 | foo.bar.should.equal('test'); 34 | }); 35 | 36 | it('does not initialize property when looked up on the prototype directly', function () { 37 | const value = Foo.prototype.bar; 38 | initializer.should.not.have.been.called; 39 | expect(value).to.be.undefined; 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /test/unit/memoize.spec.js: -------------------------------------------------------------------------------- 1 | import { stub } from 'sinon'; 2 | import memoize from '../../lib/memoize'; 3 | 4 | describe('@memoize', function () { 5 | var Foo, work, run; 6 | 7 | beforeEach(function () { 8 | work = null; 9 | Foo = class Foo { 10 | @memoize 11 | bar(...args){ 12 | return work(...args); 13 | } 14 | }; 15 | 16 | run = function (id, shouldCall, shouldReturn, args) { 17 | var inst = new Foo(); 18 | work = stub().returns(id); 19 | var ret = inst.bar.apply(inst, args); 20 | 21 | work.called.should.equal(shouldCall, `case ${id} should ${shouldCall ? '' : 'not '}be called`); 22 | ret.should.equal(shouldReturn, `case ${id} should return ${shouldReturn}`); 23 | }; 24 | }); 25 | 26 | it('works for 0 arguments', function () { 27 | run('a', true, 'a', []); 28 | run('b', false, 'a', []); 29 | }); 30 | 31 | it('works for 1 string argument', function () { 32 | run('a', true, 'a', ['x']); 33 | run('b', false, 'a', ['x']); 34 | run('c', true, 'c', ['y']); 35 | run('b', false, 'a', ['x']); 36 | }); 37 | 38 | it('works for 3 string arguments', function () { 39 | run('a', true, 'a', ['x', 'y', 'z']); 40 | run('b', false, 'a', ['x', 'y', 'z']); 41 | run('c', true, 'c', ['x', 'q', 'z']); 42 | run('d', true, 'd', ['q', 'y', 'z']); 43 | run('e', true, 'e', ['x', 'y', 'q']); 44 | }); 45 | 46 | it('works for variable string arguments', function () { 47 | run('a', true, 'a', ['x']); 48 | run('b', true, 'b', ['x', 'y']); 49 | run('c', false, 'a', ['x']); 50 | run('d', false, 'b', ['x', 'y']); 51 | run('e', true, 'e', []); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /test/unit/mixin.spec.js: -------------------------------------------------------------------------------- 1 | import mixin from '../../lib/mixin'; 2 | 3 | const BarMixin = { 4 | stuff1: 'stuff1', 5 | 6 | getStuff2() { 7 | return 'stuff2'; 8 | } 9 | }; 10 | 11 | const OverrideMixin = { 12 | get stuff5() { 13 | return 'stuff5'; 14 | }, 15 | 16 | getStuff4() { 17 | return 'stuff4-override'; 18 | } 19 | }; 20 | 21 | function applyMixins(...mixins) { 22 | @mixin(...mixins) 23 | class Foo { 24 | getStuff3() { 25 | return 'stuff3'; 26 | } 27 | 28 | getStuff4() { 29 | return 'stuff4'; 30 | } 31 | } 32 | return Foo; 33 | } 34 | 35 | describe('@mixin', function () { 36 | it('throws if you do not provide at least one mixin', function () { 37 | (function () { 38 | @mixin class Bad {}; 39 | console.error(Bad); 40 | }).should.throw('@mixin() class Bad requires at least one mixin as an argument'); 41 | 42 | (function () { 43 | @mixin() 44 | class Bad {} 45 | }).should.throw('@mixin() class Bad requires at least one mixin as an argument'); 46 | }); 47 | 48 | it('correctly adds a single mixin\'s descriptors', function () { 49 | const foo = new (applyMixins(BarMixin)); 50 | 51 | foo.stuff1.should.equal('stuff1'); 52 | foo.getStuff2().should.equal('stuff2'); 53 | foo.getStuff3().should.equal('stuff3'); 54 | foo.getStuff4().should.equal('stuff4'); 55 | }); 56 | 57 | it('correctly adds multiple mixins descriptors', function () { 58 | const foo = new (applyMixins(BarMixin, OverrideMixin)); 59 | 60 | foo.stuff1.should.equal('stuff1'); 61 | foo.getStuff2().should.equal('stuff2'); 62 | foo.getStuff3().should.equal('stuff3'); 63 | foo.getStuff4().should.equal('stuff4'); 64 | foo.stuff5.should.equal('stuff5'); 65 | }); 66 | 67 | it('correctly adds symbols', function () { 68 | const symbolHash = Symbol('mixin'); 69 | const SymbolMixin = { 70 | [symbolHash]() { 71 | return 'symbolHash'; 72 | } 73 | }; 74 | const foo = new (applyMixins(SymbolMixin)); 75 | 76 | foo[symbolHash]().should.equal('symbolHash'); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /test/unit/nonconfigurable.spec.js: -------------------------------------------------------------------------------- 1 | import nonconfigurable from '../../lib/nonconfigurable'; 2 | import readonly from '../../lib/readonly'; 3 | 4 | describe('@nonconfigurable', function () { 5 | class Foo { 6 | @nonconfigurable 7 | first() {} 8 | 9 | @nonconfigurable 10 | @readonly 11 | second() {} 12 | } 13 | 14 | it('is marked configurable: false', function () { 15 | Object.getOwnPropertyDescriptor(Foo.prototype, 'first') 16 | .configurable.should.equal(false); 17 | 18 | Object.getOwnPropertyDescriptor(Foo.prototype, 'second') 19 | .configurable.should.equal(false); 20 | }); 21 | 22 | it('causes `delete` to be noop', function () { 23 | const foo = new Foo(); 24 | const { first, second } = foo; 25 | delete foo.first; 26 | delete foo.second; 27 | foo.first.should.equal(first); 28 | foo.second.should.equal(second); 29 | }); 30 | 31 | // Only works as expected with @readonly 32 | // See https://github.com/jayphelps/core-decorators.js/issues/58 33 | it('throws if you try to reconfigure it', function () { 34 | (function () { 35 | Object.defineProperty(Foo.prototype, 'second', { 36 | value: 'I will error' 37 | }); 38 | }).should.throw('Cannot redefine property: second'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/unit/nonenumerable.spec.js: -------------------------------------------------------------------------------- 1 | import nonenumerable from '../../lib/nonenumerable'; 2 | 3 | describe('@nonenumerable', function () { 4 | class Foo { 5 | @nonenumerable 6 | bar = 'test'; 7 | } 8 | 9 | it('is not enumerable', function () { 10 | const foo = new Foo(); 11 | Object.getOwnPropertyDescriptor(foo, 'bar').enumerable.should.equal(false); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /test/unit/override.spec.js: -------------------------------------------------------------------------------- 1 | import override from '../../lib/override'; 2 | 3 | class Parent { 4 | speak(first, second) {} 5 | } 6 | 7 | describe('@override', function () { 8 | it('throws error when signature does not match', function () { 9 | (function () { 10 | class Child extends Parent { 11 | @override 12 | speak() {} 13 | } 14 | }).should.throw('Child#speak() does not properly override Parent#speak(first, second)'); 15 | }); 16 | 17 | it('throws error when no is matching name is found', function () { 18 | (function () { 19 | class Child extends Parent { 20 | @override 21 | wow() {} 22 | } 23 | }).should.throw('No descriptor matching Child#wow() was found on the prototype chain.'); 24 | }); 25 | 26 | it('throws error when no is matching name is found but suggests a closely named method if exists', function () { 27 | (function () { 28 | class Child extends Parent { 29 | @override 30 | speaks() {} 31 | } 32 | }).should.throw('No descriptor matching Child#speaks() was found on the prototype chain.\n\n Did you mean "speak"?'); 33 | }); 34 | 35 | it('does not throw an error when signatures match', function () { 36 | (function () { 37 | class Child extends Parent { 38 | @override 39 | speak(first, second) {} 40 | } 41 | }).should.not.throw(); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /test/unit/profile.spec.js: -------------------------------------------------------------------------------- 1 | import { spy, useFakeTimers } from 'sinon'; 2 | import applyDecorators from '../../lib/applyDecorators'; 3 | import profile, { defaultConsole } from '../../lib/profile'; 4 | 5 | const CONSOLE_PROFILE = defaultConsole.profile; 6 | const CONSOLE_PROFILEEND = defaultConsole.profileEnd; 7 | const CONSOLE_WARN = defaultConsole.warn; 8 | 9 | profile.__enabled = true; 10 | 11 | describe('@profile', function() { 12 | class Foo { 13 | @profile 14 | profiled() { 15 | return 'profiled'; 16 | } 17 | 18 | @profile('foo') 19 | profiledPrefix() { 20 | return; 21 | } 22 | 23 | @profile(null, true) 24 | profileOnce(cb) { 25 | return cb(); 26 | } 27 | 28 | 29 | @profile(null, 1000) 30 | profileThrottled(cb) { 31 | return cb(); 32 | } 33 | 34 | @profile(null, function () { return this.isAwesome; }) 35 | profileFunctioned(cb) { 36 | return cb(); 37 | } 38 | 39 | @profile(null, function (run) { return run; }) 40 | profileFunctionedWithParameter(run, cb) { 41 | return cb(); 42 | } 43 | 44 | unprofiled() { 45 | return; 46 | } 47 | 48 | @profile 49 | iThrowAnError() { 50 | throw 'foobar'; 51 | } 52 | }; 53 | 54 | let profileSpy; 55 | let profileEndSpy; 56 | let warnSpy; 57 | 58 | beforeEach(function () { 59 | profileSpy = defaultConsole.profile = spy(); 60 | profileEndSpy = defaultConsole.profileEnd = spy(); 61 | warnSpy = defaultConsole.warn = spy(); 62 | }); 63 | 64 | afterEach(function () { 65 | defaultConsole.profile = CONSOLE_PROFILE; 66 | defaultConsole.profileEnd = CONSOLE_PROFILEEND; 67 | defaultConsole.warn = CONSOLE_WARN; 68 | }); 69 | 70 | it('calls console.profile and console.profileEnd', function() { 71 | new Foo().profiled(); 72 | profileSpy.called.should.equal(true); 73 | profileEndSpy.called.should.equal(true); 74 | }); 75 | 76 | it('calls console.profile and console.profileEnd once when flag is on', function() { 77 | const cbSpy = spy(); 78 | 79 | const foo = new Foo(); 80 | foo.profileOnce(cbSpy); 81 | foo.profileOnce(cbSpy); 82 | profileSpy.calledOnce.should.equal(true); 83 | cbSpy.calledTwice.should.equal(true); 84 | 85 | const bar = new Foo(); 86 | bar.profileOnce(cbSpy); 87 | bar.profileOnce(cbSpy); 88 | profileSpy.calledTwice.should.equal(true); 89 | cbSpy.callCount.should.equal(4); 90 | }); 91 | 92 | it('calls console.profileEnd even if the called method throws', function() { 93 | try { 94 | new Foo().iThrowAnError(); 95 | } catch (e) { 96 | e.should.equal('foobar'); 97 | } 98 | 99 | profileSpy.called.should.equal(true); 100 | profileEndSpy.called.should.equal(true); 101 | }); 102 | 103 | it('uses class and method names for a default prefix', function() { 104 | let labelPattern = new RegExp('Foo\\.profiled'); 105 | let label; 106 | new Foo().profiled(); 107 | label = profileSpy.getCall(0).args[0]; 108 | label.should.match(labelPattern); 109 | }); 110 | 111 | it('uses a supplied prefix for the label', function() { 112 | new Foo().profiledPrefix(); 113 | profileSpy.getCall(0).args[0].should.match(/^foo/); 114 | profileEndSpy.getCall(0).args[0].should.match(/^foo/); 115 | }); 116 | 117 | it('supports a custom profileation object', function() { 118 | let profileCalled = false; 119 | let profileEndCalled = false; 120 | 121 | let myConsole = { 122 | profile(label) { 123 | profileCalled = true; 124 | }, 125 | profileEnd(label) { 126 | profileEndCalled = true; 127 | } 128 | } 129 | 130 | class Boo { 131 | @profile('custom', false, myConsole) 132 | hoo() { 133 | return; 134 | } 135 | } 136 | new Boo().hoo(); 137 | profileCalled.should.equal(true); 138 | profileEndCalled.should.equal(true); 139 | }); 140 | 141 | it('returns the value', function() { 142 | let foo = new Foo(); 143 | let result = foo.profiled(); 144 | result.should.equal('profiled'); 145 | }); 146 | 147 | describe('when throttled', function() { 148 | let clock; 149 | let count = 1; 150 | beforeEach(function() { 151 | clock = useFakeTimers(Date.now()); 152 | count += 1; 153 | }); 154 | 155 | afterEach(function() { 156 | clock.restore(); 157 | }); 158 | 159 | it('should always call function', function() { 160 | const cbSpy = spy(); 161 | const foo = new Foo(); 162 | 163 | foo.profileThrottled(cbSpy); 164 | foo.profileThrottled(cbSpy); 165 | 166 | profileSpy.calledOnce.should.equal(true); 167 | cbSpy.calledTwice.should.equal(true); 168 | }); 169 | 170 | it('should call profile after throttle time', function() { 171 | const noop = function() {}; 172 | const foo = new Foo(); 173 | 174 | foo.profileThrottled(noop); 175 | foo.profileThrottled(noop); 176 | profileSpy.calledOnce.should.equal(true); 177 | 178 | clock.tick(999); 179 | 180 | foo.profileThrottled(noop); 181 | profileSpy.calledOnce.should.equal(true); 182 | 183 | clock.tick(10); 184 | 185 | foo.profileThrottled(noop); 186 | profileSpy.calledTwice.should.equal(true); 187 | }) 188 | }); 189 | 190 | describe('when functioned', function() { 191 | it('should have `this` context', () => { 192 | const cbSpy = spy(); 193 | const foo = new Foo(); 194 | 195 | foo.profileFunctioned(cbSpy); 196 | profileSpy.calledOnce.should.equal(false); 197 | cbSpy.calledOnce.should.equal(true); 198 | 199 | foo.isAwesome = true; 200 | 201 | foo.profileFunctioned(cbSpy); 202 | profileSpy.calledOnce.should.equal(true); 203 | cbSpy.calledTwice.should.equal(true); 204 | }); 205 | 206 | it('should accept parameters', () => { 207 | const cbSpy = spy(); 208 | const foo = new Foo(); 209 | 210 | foo.profileFunctionedWithParameter(false, cbSpy); 211 | profileSpy.calledOnce.should.equal(false); 212 | cbSpy.calledOnce.should.equal(true); 213 | 214 | foo.profileFunctionedWithParameter(true, cbSpy); 215 | profileSpy.calledOnce.should.equal(true); 216 | cbSpy.calledTwice.should.equal(true); 217 | }); 218 | }); 219 | 220 | describe('when disabled', function() { 221 | class Bar { 222 | disabledProfile() { 223 | return; 224 | } 225 | } 226 | 227 | beforeEach(function() { 228 | profile.__enabled = false; 229 | }); 230 | 231 | afterEach(function() { 232 | profile.__enabled = true; 233 | }); 234 | 235 | it('should send a warning', function() { 236 | profile.__warned = false; 237 | 238 | applyDecorators(Bar, { 239 | disabledProfile: [profile] 240 | }); 241 | 242 | warnSpy.called.should.equal(true); 243 | }); 244 | 245 | it('should leave descriptor alone', function() { 246 | const oldBarDisabledProfile = Bar.prototype.disabledProfile = spy(); 247 | 248 | applyDecorators(Bar, { 249 | disabledProfile: [profile] 250 | }); 251 | 252 | Bar.prototype.disabledProfile.should.equal(oldBarDisabledProfile); 253 | }); 254 | }); 255 | 256 | }); 257 | -------------------------------------------------------------------------------- /test/unit/readonly.spec.js: -------------------------------------------------------------------------------- 1 | import readonly from '../../lib/readonly'; 2 | 3 | describe('@readonly', function () { 4 | class Foo { 5 | @readonly 6 | first() {} 7 | 8 | @readonly 9 | second = 'second'; 10 | } 11 | 12 | it('marks descriptor as writable === false', function () { 13 | Object.getOwnPropertyDescriptor(Foo.prototype, 'first') 14 | .writable.should.equal(false); 15 | }); 16 | 17 | it('makes setting property error', function () { 18 | const foo = new Foo(); 19 | 20 | (function () { 21 | foo.first = 'I will error'; 22 | }).should.throw('Cannot assign to read only property \'first\' of object \'#\''); 23 | 24 | (function () { 25 | foo.second = 'I will also error'; 26 | }).should.throw('Cannot assign to read only property \'second\' of object \'#\''); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/unit/suppress-warnings.spec.js: -------------------------------------------------------------------------------- 1 | import { spy } from 'sinon'; 2 | import suppressWarnings from '../../lib/suppress-warnings'; 3 | 4 | const CONSOLE_WARN = console.warn; 5 | 6 | describe('@suppressWarnings', function () { 7 | class Foo { 8 | @suppressWarnings 9 | suppressed(){ 10 | console.warn('a'); 11 | } 12 | 13 | unsuppressed(){ 14 | console.warn('b'); 15 | } 16 | } 17 | 18 | var warnSpy; 19 | beforeEach(function () { 20 | warnSpy = console.warn = spy(); 21 | }); 22 | 23 | afterEach(function () { 24 | console.warn = CONSOLE_WARN; 25 | }); 26 | 27 | it('suppresses warns', function () { 28 | new Foo().suppressed(); 29 | warnSpy.called.should.equal(false); 30 | }); 31 | 32 | it('restores the original console.warn', function () { 33 | new Foo().suppressed(); 34 | new Foo().unsuppressed(); 35 | warnSpy.called.should.equal(true); 36 | console.warn.should.equal(warnSpy); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/unit/throttle.spec.js: -------------------------------------------------------------------------------- 1 | import { useFakeTimers } from 'sinon'; 2 | import throttle from '../../lib/throttle'; 3 | 4 | const defaultValue = {}; 5 | 6 | class Editor { 7 | value = defaultValue; 8 | counter = 0; 9 | 10 | @throttle(500) 11 | updateCounter1(value) { 12 | this.value = value; 13 | this.counter++; 14 | } 15 | 16 | @throttle(500, { leading: false, trailing: true }) 17 | updateCounter2(value) { 18 | this.value = value; 19 | this.counter++; 20 | } 21 | 22 | @throttle(500, { leading: false, trailing: false }) 23 | updateCounter3(value) { 24 | this.value = value; 25 | this.counter++; 26 | } 27 | } 28 | 29 | describe('@throttle', function () { 30 | let editor; 31 | let clock; 32 | 33 | beforeEach(function () { 34 | editor = new Editor(); 35 | clock = useFakeTimers(Date.now()); 36 | }); 37 | 38 | afterEach(function () { 39 | clock.restore(); 40 | }) 41 | 42 | it('invokes function only once', function () { 43 | editor.updateCounter1(1); 44 | editor.counter.should.equal(1); 45 | 46 | clock.tick(600); 47 | 48 | editor.counter.should.equal(1); 49 | editor.value.should.equal(1); 50 | }); 51 | 52 | it('invokes function only twice', function () { 53 | editor.updateCounter1(1); 54 | editor.updateCounter1(2); 55 | editor.updateCounter1(3); 56 | editor.updateCounter1(4); 57 | editor.counter.should.equal(1); 58 | editor.value.should.equal(1); 59 | 60 | clock.tick(600); 61 | 62 | editor.counter.should.equal(2); 63 | editor.value.should.equal(4); 64 | }); 65 | 66 | it('invokes function delay and only once if "leading" option is false', function () { 67 | editor.updateCounter2(1); 68 | editor.counter.should.equal(0); 69 | editor.value.should.equal(defaultValue); 70 | 71 | clock.tick(400); 72 | 73 | // should still be 1 because 500ms hasn't yet passed 74 | editor.counter.should.equal(0); 75 | editor.value.should.equal(defaultValue); 76 | 77 | clock.tick(200); 78 | 79 | editor.counter.should.equal(1); 80 | editor.value.should.equal(1); 81 | }); 82 | 83 | it('uses last arguments for leading: false, trailing: true (issue #94)', function () { 84 | editor.updateCounter2(1); 85 | editor.updateCounter2(2); 86 | editor.updateCounter2(3); 87 | editor.counter.should.equal(0); 88 | editor.value.should.equal(defaultValue); 89 | 90 | clock.tick(400); 91 | 92 | // should still be 1 because 500ms hasn't yet passed 93 | editor.counter.should.equal(0); 94 | editor.value.should.equal(defaultValue); 95 | 96 | clock.tick(200); 97 | 98 | editor.counter.should.equal(1); 99 | editor.value.should.equal(3); 100 | }); 101 | 102 | it('does not invoke function if "leading" and "trailing" options are both false', function () { 103 | editor.updateCounter3(1); 104 | editor.counter.should.equal(0); 105 | editor.value.should.equal(defaultValue); 106 | 107 | clock.tick(400); 108 | 109 | // should still be 0 because leading call a canceled 110 | editor.counter.should.equal(0); 111 | editor.value.should.equal(defaultValue); 112 | editor.updateCounter3(2); 113 | 114 | clock.tick(200); 115 | 116 | // should still be 0 because trailing call a canceled 117 | editor.counter.should.equal(0); 118 | editor.value.should.equal(defaultValue); 119 | }); 120 | 121 | it('does not share timers and args between instances', function () { 122 | let editor2 = new Editor(); 123 | editor.updateCounter1(1); 124 | editor2.updateCounter1(2); 125 | 126 | editor.counter.should.equal(1); 127 | editor2.counter.should.equal(1); 128 | editor.value.should.equal(1); 129 | editor2.value.should.equal(2); 130 | 131 | clock.tick(600); 132 | 133 | editor.counter.should.equal(1); 134 | editor2.counter.should.equal(1); 135 | editor.value.should.equal(1); 136 | editor2.value.should.equal(2); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /test/unit/time.spec.js: -------------------------------------------------------------------------------- 1 | import { spy } from 'sinon'; 2 | import time, { defaultConsole } from '../../lib/time'; 3 | 4 | const CONSOLE_TIME = defaultConsole.time; 5 | const CONSOLE_TIMEEND = defaultConsole.timeEnd; 6 | 7 | describe('@time', function() { 8 | class Foo { 9 | @time 10 | timed() { 11 | return 'timed'; 12 | } 13 | 14 | @time('foo') 15 | timedPrefix() { 16 | return; 17 | } 18 | 19 | untimed() { 20 | return; 21 | } 22 | 23 | @time 24 | iThrowAnError() { 25 | throw 'foobar'; 26 | } 27 | }; 28 | 29 | let timeSpy; 30 | let timeEndSpy; 31 | 32 | beforeEach(function () { 33 | timeSpy = defaultConsole.time = spy(); 34 | timeEndSpy = defaultConsole.timeEnd = spy(); 35 | }); 36 | 37 | afterEach(function () { 38 | defaultConsole.time = CONSOLE_TIME; 39 | defaultConsole.timeEnd = CONSOLE_TIMEEND; 40 | }); 41 | 42 | it('calls console.time and console.timeEnd', function() { 43 | new Foo().timed(); 44 | timeSpy.called.should.equal(true); 45 | timeEndSpy.called.should.equal(true); 46 | }); 47 | 48 | it('calls console.timeEnd even if the called method throws', function() { 49 | try { 50 | new Foo().iThrowAnError(); 51 | } catch (e) { 52 | e.should.equal('foobar'); 53 | } 54 | 55 | timeSpy.called.should.equal(true); 56 | timeEndSpy.called.should.equal(true); 57 | }); 58 | 59 | it('uses class and method names for a default prefix', function() { 60 | let labelPattern = new RegExp('Foo\\.timed-\\d+'); 61 | let label; 62 | new Foo().timed(); 63 | label = timeSpy.getCall(0).args[0]; 64 | label.should.match(labelPattern); 65 | }); 66 | 67 | it('creates a unique label with a counter', function() { 68 | let dashNum = new RegExp('.*-(\\d+)$'); 69 | let firstNum; 70 | let secondNum; 71 | new Foo().timed(); 72 | new Foo().timedPrefix(); 73 | firstNum = parseInt(timeSpy.getCall(0).args[0].replace(dashNum, '$1'), 10); 74 | secondNum = parseInt(timeSpy.getCall(1).args[0].replace(dashNum, '$1'), 10); 75 | secondNum.should.equal(firstNum + 1); 76 | }); 77 | 78 | it('uses a supplied prefix for the label', function() { 79 | new Foo().timedPrefix(); 80 | timeSpy.getCall(0).args[0].should.match(/^foo-/); 81 | timeEndSpy.getCall(0).args[0].should.match(/^foo-/); 82 | }); 83 | 84 | it('supports a custom timeation object', function() { 85 | let timeCalled = false; 86 | let timeEndCalled = false; 87 | 88 | let myConsole = { 89 | time(label) { 90 | timeCalled = true; 91 | }, 92 | timeEnd(label) { 93 | timeEndCalled = true; 94 | } 95 | } 96 | 97 | class Boo { 98 | @time('custom', myConsole) 99 | hoo() { 100 | return; 101 | } 102 | } 103 | new Boo().hoo(); 104 | timeCalled.should.equal(true); 105 | timeEndCalled.should.equal(true); 106 | }); 107 | 108 | it('returns the value', function() { 109 | let foo = new Foo(); 110 | let result = foo.timed(); 111 | result.should.equal('timed'); 112 | }); 113 | 114 | }); 115 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "exclude": ["scripts", "lib/**", "es", "node_modules", "built" ], 4 | "compilerOptions": { 5 | "outDir": "built", 6 | "target": "es5", 7 | "module":"commonjs", 8 | "sourceMap": true, 9 | "allowJs": true, 10 | "checkJs": false, 11 | "allowUnreachableCode": true, 12 | "lib": [ 13 | "es5", 14 | "dom", 15 | "es2015","es2015.core", "es2015.collection" 16 | ], 17 | "baseUrl": ".", 18 | "paths": { 19 | "lib/*": ["src/*"] 20 | }, 21 | "rootDirs": [ 22 | "src", "lib" 23 | ], 24 | "experimentalDecorators": true, 25 | "allowSyntheticDefaultImports": true 26 | }, 27 | "compileOnSave": true 28 | } 29 | --------------------------------------------------------------------------------