diff --git a/.gitignore b/.gitignore index ad46b30..fa7b2a6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,61 +1,4 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories +coverage/ node_modules/ -jspm_packages/ - -# TypeScript v1 declaration files -typings/ - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variables file -.env - -# next.js build output -.next +.nyc_output/ +package-lock.json diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..187b287 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: node_js +node_js: + - 8 + - 10 + - node +script: npm run ci diff --git a/README.md b/README.md index 29eb96e..e55e029 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,44 @@ -# incomplete-symbol -Custom-remove features of a Symbol implementation. +# incomplete-symbol [![NPM Version][npm-image]][npm-url] [![Build Status][travis-image]][travis-url] [![Coverage Status][coveralls-image]][coveralls-url] + +> Custom-remove features of a [`Symbol`](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Symbol) implementation. + + +This is useful when simulating the incomplete `Symbol` implementations available in some of today's modern web browsers. + + +## Installation + +[Node.js](http://nodejs.org/) `>= 8` is required. To install, type this at the command line: +```shell +npm install incomplete-symbol +``` + + +## Usage + +```js +const customizeSymbol = require('incomplete-symbol'); + +const exclusions = ['description', 'toStringTag']; +const IncompleteSymbol = customizeSymbol(exclusions); +const symbol = new IncompleteSymbol('foo'); + +console.log(IncompleteSymbol.toStringTag); //-> undefined +console.log(symbol.description); //-> undefined +``` + + +## Arguments + +### `exclusions` +Type: `Array` +Default value: `[]` +The output `Symbol` function and any instances created with it will not expose each listed property/method. + + +[npm-image]: https://img.shields.io/npm/v/incomplete-symbol.svg +[npm-url]: https://npmjs.com/package/incomplete-symbol +[travis-image]: https://img.shields.io/travis/stevenvachon/incomplete-symbol.svg +[travis-url]: https://travis-ci.org/stevenvachon/incomplete-symbol +[coveralls-image]: https://img.shields.io/coveralls/stevenvachon/incomplete-symbol.svg +[coveralls-url]: https://coveralls.io/github/stevenvachon/incomplete-symbol diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..a57ff0b --- /dev/null +++ b/lib/index.js @@ -0,0 +1,46 @@ +"use strict"; +const {instanceMethods, instanceProperties, staticMethods, staticProperties} = require("./props"); + + + +const customizeSymbol = (exclusions=[]) => +{ + // For testing: uses the global value when this function is called + // instead of when instances are created + const globalSymbol = Symbol; + + function IncompleteSymbolInstance(description) + { + this._symbol = globalSymbol(description); + + instanceMethods + .filter(method => !exclusions.includes(method) && typeof this._symbol[method]==="function") + .forEach(method => this[method] = (...args) => this._symbol[method](...args)); + + instanceProperties + .filter(property => !exclusions.includes(property) && this._symbol[property]!==undefined) + .forEach(property => Object.defineProperty(this, property, + { + get: () => this._symbol[property] + })); + } + + const IncompleteSymbol = (...args) => new IncompleteSymbolInstance(...args); + + staticMethods + .filter(method => !exclusions.includes(method) && typeof globalSymbol[method]==="function") + .forEach(method => IncompleteSymbol[method] = (...args) => globalSymbol[method](...args)); + + staticProperties + .filter(property => !exclusions.includes(property) && property in globalSymbol) + .forEach(property => Object.defineProperty(IncompleteSymbol, property, + { + get: () => globalSymbol[property] + })); + + return IncompleteSymbol; +}; + + + +module.exports = customizeSymbol; diff --git a/lib/props.js b/lib/props.js new file mode 100644 index 0000000..16aad86 --- /dev/null +++ b/lib/props.js @@ -0,0 +1,54 @@ +"use strict"; + + + +const instanceMethods = +[ + "toString", + "valueOf", + Symbol.toPrimitive +]; + + + +const instanceProperties = +[ + "constructor", + "description" +]; + + + +const staticMethods = +[ + "for", + "keyFor" +]; + + + +const staticProperties = +[ + "hasInstance", + "isConcatSpreadable", + "iterator", + "length", + "match", + "replace", + "search", + "species", + "split", + "toPrimitive", + "toStringTag", + "unscopables" +]; + + + +module.exports = +{ + instanceMethods, + instanceProperties, + staticMethods, + staticProperties +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..3887c99 --- /dev/null +++ b/package.json @@ -0,0 +1,32 @@ +{ + "name": "incomplete-symbol", + "description": "Custom-remove features of a Symbol implementation.", + "version": "1.0.0", + "license": "MIT", + "author": "Steven Vachon (https://www.svachon.com/)", + "repository": "github:stevenvachon/incomplete-symbol", + "main": "lib", + "devDependencies": { + "chai": "^4.2.0", + "coveralls": "^3.0.2", + "mocha": "^5.2.0", + "nyc": "^13.1.0", + "sinon": "^7.2.2", + "sinon-chai": "^3.3.0", + "symbol.prototype.description": "^1.0.0" + }, + "engines": { + "node": ">= 8" + }, + "scripts": { + "ci": "npm run test && nyc report --reporter=text-lcov | coveralls", + "posttest": "nyc report --reporter=html", + "test": "nyc --reporter=text-summary mocha test --check-leaks --bail" + }, + "files": [ + "lib" + ], + "keywords": [ + "symbol" + ] +} diff --git a/test.js b/test.js new file mode 100644 index 0000000..f61042f --- /dev/null +++ b/test.js @@ -0,0 +1,142 @@ +"use strict"; +const customizeSymbol = require("./lib"); +const {afterEach, beforeEach, describe, it} = require("mocha"); +const {expect} = require("chai").use( require("sinon-chai") ); +const {instanceMethods, instanceProperties, staticMethods, staticProperties} = require("./lib/props"); +const {restore, stub} = require("sinon"); +require("symbol.prototype.description/auto"); + + + +const instanceKeys = [...instanceMethods, ...instanceProperties]; + +const propertyString = key => typeof key === "symbol" ? key.description : `"${key}"`; + +const removeExtendableKeys = keys => keys.filter(key => unextendableKeys.includes(key)); +const removeUnextendableKeys = keys => keys.filter(key => !unextendableKeys.includes(key)); + +const staticKeys = [...staticMethods, ...staticProperties]; +const symbolString = "description"; + +const unextendableKeys = +[ + "constructor", + "length", + "toString", + "valueOf" +]; + + + +describe("Default behavior", () => +{ + it("is the same as the global", () => + { + const IncompleteSymbol = customizeSymbol(); + + const symbol = IncompleteSymbol(symbolString); + + instanceKeys.forEach(key => expect(symbol).to.have.property(key)); + staticKeys.forEach(key => expect(IncompleteSymbol).to.have.property(key)); + + const args = ["arg1", "arg2"]; + + instanceMethods.forEach(method => + { + stub(Symbol.prototype, method); + + symbol[method](...args); + + expect(Symbol.prototype[method]).to.have.been.calledWith(...args); + + restore(); + }); + + staticMethods.forEach(method => + { + stub(Symbol, method); + + IncompleteSymbol[method](...args); + + expect(Symbol[method]).to.have.been.calledWith(...args); + + restore(); + }); + }); +}); + + + +describe("Unavailable properties/methods", () => +{ + const originalSymbol = Symbol; + + + + beforeEach(() => + { + const stubbedSymbol = customizeSymbol( + [ + "description", + "for", + "hasInstance", + Symbol.toPrimitive + ]); + + stub(global, "Symbol").callsFake(stubbedSymbol); + }); + + afterEach(() => restore()); + + + + it("does not attempt to wrap them", () => + { + const IncompleteSymbol = customizeSymbol(); + + const symbol = IncompleteSymbol(symbolString); + + expect(IncompleteSymbol).to.not.have.property("for"); + expect(IncompleteSymbol).to.not.have.property("hasInstance"); + expect(symbol).to.not.have.property("description"); + expect(symbol).to.not.have.property(originalSymbol.toPrimitive); + }) +}); + + + +describe("Exclusions", () => +{ + removeUnextendableKeys(instanceKeys).forEach(key => it(propertyString(key), () => + { + const IncompleteSymbol = customizeSymbol([key]); + + const symbol = IncompleteSymbol(symbolString); + + expect(symbol).to.not.have.property(key); + })); + + + + removeUnextendableKeys(staticKeys).forEach(key => it(propertyString(key), () => + { + const IncompleteSymbol = customizeSymbol([key]); + + expect(IncompleteSymbol).to.not.have.property(key); + })); + + + + it("all methods/properties", () => + { + const IncompleteSymbol = customizeSymbol([...instanceKeys, ...staticKeys]); + + const symbol = IncompleteSymbol(symbolString); + + removeExtendableKeys(instanceKeys).forEach(key => expect(symbol).to.have.property(key)); + removeExtendableKeys(staticKeys).forEach(key => expect(IncompleteSymbol).to.have.property(key)); + + removeUnextendableKeys(instanceKeys).forEach(key => expect(symbol).to.not.have.property(key)); + removeUnextendableKeys(staticKeys).forEach(key => expect(IncompleteSymbol).to.not.have.property(key)); + }); +});