diff options
| -rw-r--r-- | .editorconfig | 18 | ||||
| -rw-r--r-- | .eslintrc | 52 | ||||
| -rw-r--r-- | .github/workflows/compressed-size.yml | 12 | ||||
| -rw-r--r-- | .github/workflows/main.yml | 25 | ||||
| -rw-r--r-- | .gitignore | 5 | ||||
| -rw-r--r-- | LICENSE | 21 | ||||
| -rw-r--r-- | README.md | 188 | ||||
| -rw-r--r-- | package.json | 100 | ||||
| -rw-r--r-- | src/index.ts | 239 | ||||
| -rw-r--r-- | test/index_test.ts | 204 | ||||
| -rw-r--r-- | test/test-types-compilation.ts | 78 | ||||
| -rw-r--r-- | tsconfig.json | 17 |
12 files changed, 784 insertions, 175 deletions
diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..04d7ef9 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +root = true + +[*] +indent_style = tab +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[{package.json,.*rc,*.yml}] +indent_style = space +indent_size = 2 +insert_final_newline = false + +[*.md] +trim_trailing_whitespace = false +indent_style = space +indent_size = 2 diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..429b835 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,52 @@ +{ + "ignorePatterns": [ + "node_modules", + "dist", + "index.d.ts" + ], + "extends": [ + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "developit" + ], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "sourceType": "module" + }, + "env": { + "browser": true, + "mocha": true, + "jest": false, + "es6": true + }, + "globals": { + "expect": true + }, + "rules": { + "semi": [ + 2, + "always" + ], + "brace-style": [ + 2, + "1tbs" + ], + "quotes": [ + 2, + "single" + ], + "lines-around-comment": [ + 2, + { + "allowBlockStart": true, + "allowObjectStart": true + } + ], + "jest/valid-expect": 0, + "@typescript-eslint/no-explicit-any": 0, + "@typescript-eslint/explicit-function-return-type": 0, + "@typescript-eslint/explicit-module-boundary-types": 0, + "@typescript-eslint/no-empty-function": 0, + "@typescript-eslint/no-non-null-assertion": 0 + } +} diff --git a/.github/workflows/compressed-size.yml b/.github/workflows/compressed-size.yml new file mode 100644 index 0000000..45cad2f --- /dev/null +++ b/.github/workflows/compressed-size.yml @@ -0,0 +1,12 @@ +name: Compressed Size + +on: [pull_request] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: preactjs/compressed-size-action@v2 + with: + pattern: "./dist/*.{js,mjs,cjs}" diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..7b73b7d --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,25 @@ +name: CI + +on: + pull_request: + branches: + - "**" + push: + branches: + - main + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: 14 + - name: npm install, build, and test + run: | + npm install + npm run build --if-present + npm test + env: + CI: true @@ -1,4 +1,9 @@ /dist /node_modules +/npm-debug.log +/index.d.ts +package-lock.json .DS_Store +.idea +.vscode .*cache* @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2021 Jason Miller + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. @@ -1,29 +1,21 @@ # Nitt -> Small, functional event emitter / pubsub. +> Small functional event emitter / pubsub. -- **Small:** less than 300 bytes gzipped -- **Familiar:** similar names (on, off, once, emit) & ideas as [Node's EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) -- **Functional:** methods don't rely on `this`, unless you want to... -- **No dependency:** Nitt has no external dependencies +* **Microscopic:** weighs less than 200 bytes gzipped +* **Useful:** a wildcard `"*"` event type listens to all events +* **Familiar:** same names & ideas as [Node's EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter) + +nitt was made for the browser, but works in any JavaScript runtime. It has no dependencies and supports IE9+. ## Table of Contents -- [Nitt](#nitt) - - [Table of Contents](#table-of-contents) - - [Install](#install) - - [Usage](#usage) - - [API](#api) - - [nitt](#nitt) - - [on](#on) - - [once](#once) - - [when](#when) - - [off](#off) - - [emit](#emit) - - [Caveats](#caveats) - - [Contribute](#contribute) - - [Acknowledments](#acknowledments) - - [License](#license) +* [Install](#install) +* [Usage](#usage) +* [Examples & Demos](#examples--demos) +* [API](#api) +* [Contribute](#contribute) +* [License](#license) ## Install @@ -33,107 +25,163 @@ This project uses [node](http://nodejs.org) and [npm](https://npmjs.com). Go che $ npm install --save nitt ``` +Then with a module bundler like [rollup](http://rollupjs.org/) or [webpack](https://webpack.js.org/), use as you would anything else: + +```javascript +// using ES6 modules +import nitt from 'nitt' + +// using CommonJS modules +var nitt = require('nitt') +``` + +The [UMD](https://github.com/umdjs/umd) build is also available on [unpkg](https://unpkg.com): + +```html +<script src="https://unpkg.com/nitt/dist/nitt.umd.js"></script> +``` + +You can find the library on `window.nitt`. + ## Usage ```js -import nitt from 'nitt'; +import nitt from 'nitt' -const emitter = nitt(); +const emitter = nitt() // listen to an event -emitter.on('foo', e => console.log('foo', e)); +emitter.on('foo', e => console.log('foo', e) ) // listen to all events -emitter.on('*', (type, e) => console.log(type, e)); - -// listen to a single event -emitter.once('foo', e => console.log('foo', e)); +emitter.on('*', (type, e) => console.log(type, e) ) // fire an event -emitter.emit('foo', { a: 'b' }); +emitter.emit('foo', { a: 'b' }) + +// clearing all events +emitter.all.clear() // working with handler references: function onFoo() {} -emitter.on('foo', onFoo); // listen -emitter.off('foo', onFoo); // unlisten -emitter.once('foo', onFoo); // listen to a single event - -// using a promise -const promise = emitter.when('bar'); -promise.then(evt => console.log(evt)); -emitter.emit('bar', 'done'); // prints 'done' in the console +emitter.on('foo', onFoo) // listen +emitter.off('foo', onFoo) // unlisten ``` -## API +### Typescript -### nitt +Set `"strict": true` in your tsconfig.json to get improved type inference for `nitt` instance methods. -Nitt: Small (<300B) functional event emitter / pubsub. +```ts +import nitt from 'nitt'; -**Parameters** +type Events = { + foo: string; + bar?: number; +}; -- `all` **EventHandlerMap** +const emitter = nitt<Events>(); // inferred as emitter<Events> -Returns **Nitt** +emitter.on('foo', (e) => {}); // 'e' has inferred type 'string' -### on +emitter.emit('foo', 42); // Error: Argument of type 'number' is not assignable to parameter of type 'string'. (2345) +``` -Register an event handler for the given type. +Alternatively, you can use the provided `emitter` type: -**Parameters** +```ts +import nitt, { emitter } from 'nitt'; -- `type` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Type of event to listen for, or `"*"` for all events -- `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Function to call in response to given event +type Events = { + foo: string; + bar?: number; +}; -### once +const emitter: emitter<Events> = nitt<Events>(); +``` + +## API -Register an event handler that is executed just once. +<!-- Generated by documentation.js. Update this documentation by updating the source code. --> -**Parameters** +#### Table of Contents -- `type` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Type of event to listen for, or `"*"` for all events -- `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Function to call in response to given event +* [nitt](#nitt) +* [all](#all) +* [on](#on) + * [Parameters](#parameters) +* [off](#off) + * [Parameters](#parameters-1) +* [emit](#emit) + * [Parameters](#parameters-2) +* [once](#once) + * [Parameters](#parameters-3) +* [when](#when) + * [Parameters](#parameters-4) -### when +### nitt -Returns a promise for a single event +Nitt: Tiny (~200b) functional event emitter / pubsub. + +Returns **Nitt**  -**Parameters** +### all -- `type` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Type of event to listen for, or `"*"` for all events +A Map of event names to registered handler functions. -Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)** +### on + +Register an event handler for the given type. + +#### Parameters + +* `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) | [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** Type of event to listen for, or `'*'` for all events +* `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Function to call in response to given event ### off Remove an event handler for the given type. +If `handler` is omitted, all handlers of the given type are removed. -**Parameters** +#### Parameters -- `type` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Type of event to unregister `handler` from, or `"*"` -- `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Handler function to remove +* `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) | [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** Type of event to unregister `handler` from (`'*'` to remove a wildcard handler) +* `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)?** Handler function to remove ### emit Invoke all handlers for the given type. -If present, `"*"` handlers are invoked after type-matched handlers. +If present, `'*'` handlers are invoked after type-matched handlers. -**Parameters** +Note: Manually firing '\*' handlers is not supported. -- `type` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** The event type to invoke -- `evt` **Any?** Any value (object is recommended and powerful), passed to each handler +#### Parameters -## Caveats +* `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) | [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** The event type to invoke +* `evt` **Any?** Any value (object is recommended and powerful), passed to each handler -Keep in mind, due to the nature, of the once handlers that self deregister, you are currently not able to remove (off) a once / when handlers. +### once -## Contribute +Register an event handler that is executed just once for the given type. + +#### Parameters + +* `type` **([string](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String) | [symbol](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Symbol))** Type of event to listen for, or `'*'` for all events +* `handler` **[Function](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function)** Function to call in response to given event + +### when -If you want to contribute, I would be happy to look in to your PRs. +Returns a promise for a single event + +#### Parameters + +* `type` **[String](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String)** Type of event to listen for, or `"*"` for any event -## Acknowledments +Returns **[Promise](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise)\<any>**  + +## Contribute -I would like to thank [Jason Miller](https://jasonformat.com/) (@developit) for developing `mitt`. `nitt` is no more than just `mitt` with a few extras. +For reporting issues and submitting patches, send an email. ## License diff --git a/package.json b/package.json index d7268b4..ef26fa0 100644 --- a/package.json +++ b/package.json @@ -1,34 +1,88 @@ { "name": "nitt", - "version": "1.0.2", - "author": "Marin Ivanov <[email protected]>", - "license": "MIT", + "version": "1.1.0", + "description": "Tiny 200b functional Event Emitter / pubsub.", + "module": "dist/nitt.mjs", + "main": "dist/nitt.js", + "jsnext:main": "dist/nitt.mjs", + "umd:main": "dist/nitt.umd.js", + "source": "src/index.ts", + "typings": "index.d.ts", + "exports": { + "types": "./index.d.ts", + "module": "./dist/nitt.mjs", + "import": "./dist/nitt.mjs", + "require": "./dist/nitt.js", + "default": "./dist/nitt.mjs" + }, + "scripts": { + "test": "npm-run-all --silent typecheck lint mocha test-types", + "mocha": "mocha test", + "test-types": "tsc test/test-types-compilation.ts --noEmit --strict", + "lint": "eslint src test --ext ts --ext js", + "typecheck": "tsc --noEmit", + "bundle": "microbundle -f es,cjs,umd", + "build": "npm-run-all --silent clean -p bundle -s docs", + "clean": "rimraf dist", + "docs": "documentation readme src/index.ts --section API -q --parse-extension ts", + "release": "npm run -s build -s && npm t && git commit -am $npm_package_version && git tag $npm_package_version && git push && git push --tags && npm publish" + }, "repository": { "type": "git", - "url": "https://github.com/metala/nitt.git" + "url": "https://pgit.metala.org/lib/nitt.git" }, "keywords": [ - "nitt", - "event-listener", - "event-emitter", - "eventemitter" + "events", + "eventemitter", + "emitter", + "pubsub" ], - "scripts": { - "build": "microbundle", - "dev": "microbundle watch", - "test": "jest" + "authors": [ + "Marin Ivanov <[email protected]>", + "Jason Miller <[email protected]>" + ], + "license": "MIT", + "files": [ + "dist", + "index.d.ts" + ], + "mocha": { + "extension": [ + "ts" + ], + "require": [ + "ts-node/register", + "esm" + ], + "spec": [ + "test/*_test.ts" + ] + }, + "prettier": { + "singleQuote": true, + "trailingComma": "none" }, - "source": "src/index.ts", - "main": "dist/nitt.js", - "module": "dist/nitt.esm.js", - "unpkg": "dist/nitt.umd.js", "devDependencies": { - "@types/jest": "^24.0.11", - "@types/node": "^11.13.7", - "documentation": "^10.1.0", - "jest": "^24.7.1", - "microbundle": "^0.11.0", - "ts-jest": "^24.0.2", - "typescript": "^3.4.5" + "@types/chai": "^4.2.11", + "@types/mocha": "^7.0.2", + "@types/sinon": "^9.0.4", + "@types/sinon-chai": "^3.2.4", + "@typescript-eslint/eslint-plugin": "^5.61.0", + "@typescript-eslint/parser": "^5.61.0", + "chai": "^4.2.0", + "documentation": "^14.0.2", + "eslint": "^7.32.0", + "eslint-config-developit": "^1.2.0", + "eslint-plugin-compat": "^4.1.4", + "esm": "^3.2.25", + "microbundle": "^0.12.3", + "mocha": "^8.0.1", + "npm-run-all": "^4.1.5", + "prettier": "^2.8.8", + "rimraf": "^3.0.2", + "sinon": "^9.0.2", + "sinon-chai": "^3.5.0", + "ts-node": "^10.9.1", + "typescript": "^4.9.5" } } diff --git a/src/index.ts b/src/index.ts index 722412b..7aa6826 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,86 +1,167 @@ +export type EventType = string | symbol; + // An event handler can take an optional event argument // and should not return a value -type EventHandler = (event?: any) => void; -type WildCardEventHandler = (type: string, event?: any) => void; +export type Handler<T = unknown> = (event: T) => void; +export type WildcardHandler<T = Record<string, unknown>> = ( + type: keyof T, + event: T[keyof T] +) => void; // An array of all currently registered event handlers for a type -type EventHandlerList = Array<EventHandler>; -type WildCardEventHandlerList = Array<WildCardEventHandler>; +export type EventHandlerList<T = unknown> = Array<Handler<T>>; +export type WildCardEventHandlerList<T = Record<string, unknown>> = Array< + WildcardHandler<T> +>; + // A map of event types and their corresponding event handlers. -type EventHandlerMap = { - '*'?: WildCardEventHandlerList; - [type: string]: EventHandlerList; -}; - -/** Nitt: functional event emitter / pubsub. - * @name nitt - * @returns {Nitt} +export type EventHandlerMap<Events extends Record<EventType, unknown>> = Map< + keyof Events | '*', + EventHandlerList<Events[keyof Events]> | WildCardEventHandlerList<Events> +>; + +export interface Emitter<Events extends Record<EventType, unknown>> { + all: EventHandlerMap<Events>; + + on<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void; + on(type: '*', handler: WildcardHandler<Events>): void; + + once<Key extends keyof Events>(type: Key, handler: Handler<Events[Key]>): void; + once(type: '*', handler: WildcardHandler<Events>): void; + + off<Key extends keyof Events>( + type: Key, + handler?: Handler<Events[Key]> + ): void; + off(type: '*', handler: WildcardHandler<Events>): void; + + emit<Key extends keyof Events>(type: Key, event: Events[Key]): void; + emit<Key extends keyof Events>( + type: undefined extends Events[Key] ? Key : never + ): void; + + when<Key extends keyof Events>(type: Key): Promise<void>; +} + +/** + * Nitt: Tiny (~200b) functional event emitter / pubsub. + * @name nitt + * @returns {Nitt} */ -export default function nitt(all: EventHandlerMap) { - all = all || Object.create(null); - - return { - /** - * Register an event handler for the given type. - * - * @param {String} type Type of event to listen for, or `"*"` for all events - * @param {Function} handler Function to call in response to given event - */ - on(type: string, handler: EventHandler) { - (all[type] || (all[type] = [])).push(handler); - }, - - /** - * Register an event handler that is executed just once. - * - * @param {String} type Type of event to listen for, or `"*"` for any event - * @param {Function} handler Function to call in response to given event - */ - once(type: string, handler: EventHandler) { - const onceHandler = (evt: any) => { - handler(evt); - this.off(type, onceHandler); - }; - - this.on(type, onceHandler); - }, - - /** - * Returns a promise for a single event - * - * @param {String} type Type of event to listen for, or `"*"` for any event - * @returns {Promise<any>} - */ - when(type: string): Promise<any> { - return new Promise<any>(resolve => this.once(type, resolve)); - }, - - /** - * Remove an event handler for the given type. - * - * @param {String} type Type of event to unregister `handler` from, or `"*"` - * @param {Function} handler Handler function to remove - */ - off(type: string, handler: EventHandler) { - if (all[type]) { - all[type].splice(all[type].indexOf(handler) >>> 0, 1); - } - }, - - /** - * Invoke all handlers for the given type. - * If present, `"*"` handlers are invoked after type-matched handlers. - * - * @param {String} type The event type to invoke - * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler - */ - emit(type: string, evt: any) { - (all[type] || []).forEach(handler => { - handler(evt); - }); - (all['*'] || []).forEach(handler => { - handler(type, evt); - }); - }, - }; +export default function nitt<Events extends Record<EventType, unknown>>( + all?: EventHandlerMap<Events> +): Emitter<Events> { + type GenericEventHandler = + | Handler<Events[keyof Events]> + | WildcardHandler<Events>; + all = all || new Map(); + + return { + /** + * A Map of event names to registered handler functions. + */ + all, + + /** + * Register an event handler for the given type. + * @param {string|symbol} type Type of event to listen for, or `'*'` for all events + * @param {Function} handler Function to call in response to given event + * @memberOf nitt + */ + on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) { + const handlers: Array<GenericEventHandler> | undefined = all!.get(type); + if (handlers) { + handlers.push(handler); + } else { + all!.set(type, [handler] as EventHandlerList<Events[keyof Events]>); + } + }, + + /** + * Remove an event handler for the given type. + * If `handler` is omitted, all handlers of the given type are removed. + * @param {string|symbol} type Type of event to unregister `handler` from (`'*'` to remove a wildcard handler) + * @param {Function} [handler] Handler function to remove + * @memberOf nitt + */ + off<Key extends keyof Events>(type: Key, handler?: GenericEventHandler) { + const handlers: Array<GenericEventHandler> | undefined = all!.get(type); + if (handlers) { + if (handler) { + handlers.splice(handlers.indexOf(handler) >>> 0, 1); + } else { + all!.set(type, []); + } + } + }, + + /** + * Invoke all handlers for the given type. + * If present, `'*'` handlers are invoked after type-matched handlers. + * + * Note: Manually firing '*' handlers is not supported. + * + * @param {string|symbol} type The event type to invoke + * @param {Any} [evt] Any value (object is recommended and powerful), passed to each handler + * @memberOf nitt + */ + emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) { + let handlers = all!.get(type); + if (handlers) { + (handlers as EventHandlerList<Events[keyof Events]>) + .slice() + .map((handler) => { + handler(evt!); + }); + } + + handlers = all!.get('*'); + if (handlers) { + (handlers as WildCardEventHandlerList<Events>) + .slice() + .map((handler) => { + handler(type, evt!); + }); + } + }, + + /** + * Register an event handler that is executed just once for the given type. + * + * @param {string|symbol} type Type of event to listen for, or `'*'` for all events + * @param {Function} handler Function to call in response to given event + * @memberOf nitt + */ + once<Key extends keyof Events>(type: Key, handler: GenericEventHandler) { + if (type === '*') { + const onceHandler: WildcardHandler<Events> = (type: keyof Events, event: Events[keyof Events]) => { + (handler as WildcardHandler<Events>)(type, event); + this.off('*', onceHandler); + }; + this.on('*', onceHandler); + } else { + const onceHandler = (evt: Events[Key]) => { + (handler as Handler<Events[Key]>)(evt); + this.off(type, onceHandler); + }; + this.on(type, onceHandler); + } + }, + + /** + * Returns a promise for a single event + * + * @param {String} type Type of event to listen for, or `"*"` for any event + * @returns {Promise<any>} + */ + when<Key extends keyof Events>(type: Key): Promise<any> { + return new Promise((resolve: GenericEventHandler) => { + if (type === '*') { + this.once('*', (resolve as WildcardHandler<Events>)); + } else { + this.once(type, (resolve as Handler<Events[Key]>)); + } + }); + } + }; } diff --git a/test/index_test.ts b/test/index_test.ts new file mode 100644 index 0000000..4a67fe6 --- /dev/null +++ b/test/index_test.ts @@ -0,0 +1,204 @@ +import nitt, { Emitter, EventHandlerMap } from '..'; +import chai, { expect } from 'chai'; +import { spy } from 'sinon'; +import sinonChai from 'sinon-chai'; +chai.use(sinonChai); + +describe('nitt', () => { + it('should default export be a function', () => { + expect(nitt).to.be.a('function'); + }); + + it('should accept an optional event handler map', () => { + expect(() => nitt(new Map())).not.to.throw; + const map = new Map(); + const a = spy(); + const b = spy(); + map.set('foo', [a, b]); + const events = nitt<{ foo: undefined }>(map); + events.emit('foo'); + expect(a).to.have.been.calledOnce; + expect(b).to.have.been.calledOnce; + }); +}); + +describe('nitt#', () => { + const eventType = Symbol('eventType'); + type Events = { + foo: unknown; + constructor: unknown; + FOO: unknown; + bar: unknown; + Bar: unknown; + 'baz:bat!': unknown; + 'baz:baT!': unknown; + Foo: unknown; + [eventType]: unknown; + }; + let events: EventHandlerMap<Events>, inst: Emitter<Events>; + + beforeEach(() => { + events = new Map(); + inst = nitt(events); + }); + + describe('properties', () => { + it('should expose the event handler map', () => { + expect(inst).to.have.property('all').that.is.a('map'); + }); + }); + + describe('on()', () => { + it('should be a function', () => { + expect(inst).to.have.property('on').that.is.a('function'); + }); + + it('should register handler for new type', () => { + const foo = () => {}; + inst.on('foo', foo); + + expect(events.get('foo')).to.deep.equal([foo]); + }); + + it('should register handlers for any type strings', () => { + const foo = () => {}; + inst.on('constructor', foo); + + expect(events.get('constructor')).to.deep.equal([foo]); + }); + + it('should append handler for existing type', () => { + const foo = () => {}; + const bar = () => {}; + inst.on('foo', foo); + inst.on('foo', bar); + + expect(events.get('foo')).to.deep.equal([foo, bar]); + }); + + it('should NOT normalize case', () => { + const foo = () => {}; + inst.on('FOO', foo); + inst.on('Bar', foo); + inst.on('baz:baT!', foo); + + expect(events.get('FOO')).to.deep.equal([foo]); + expect(events.has('foo')).to.equal(false); + expect(events.get('Bar')).to.deep.equal([foo]); + expect(events.has('bar')).to.equal(false); + expect(events.get('baz:baT!')).to.deep.equal([foo]); + }); + + it('can take symbols for event types', () => { + const foo = () => {}; + inst.on(eventType, foo); + expect(events.get(eventType)).to.deep.equal([foo]); + }); + + // Adding the same listener multiple times should register it multiple times. + // See https://nodejs.org/api/events.html#events_emitter_on_eventname_listener + it('should add duplicate listeners', () => { + const foo = () => {}; + inst.on('foo', foo); + inst.on('foo', foo); + expect(events.get('foo')).to.deep.equal([foo, foo]); + }); + }); + + describe('off()', () => { + it('should be a function', () => { + expect(inst).to.have.property('off').that.is.a('function'); + }); + + it('should remove handler for type', () => { + const foo = () => {}; + inst.on('foo', foo); + inst.off('foo', foo); + + expect(events.get('foo')).to.be.empty; + }); + + it('should NOT normalize case', () => { + const foo = () => {}; + inst.on('FOO', foo); + inst.on('Bar', foo); + inst.on('baz:bat!', foo); + + inst.off('FOO', foo); + inst.off('Bar', foo); + inst.off('baz:baT!', foo); + + expect(events.get('FOO')).to.be.empty; + expect(events.has('foo')).to.equal(false); + expect(events.get('Bar')).to.be.empty; + expect(events.has('bar')).to.equal(false); + expect(events.get('baz:bat!')).to.have.lengthOf(1); + }); + + it('should remove only the first matching listener', () => { + const foo = () => {}; + inst.on('foo', foo); + inst.on('foo', foo); + inst.off('foo', foo); + expect(events.get('foo')).to.deep.equal([foo]); + inst.off('foo', foo); + expect(events.get('foo')).to.deep.equal([]); + }); + + it('off("type") should remove all handlers of the given type', () => { + inst.on('foo', () => {}); + inst.on('foo', () => {}); + inst.on('bar', () => {}); + inst.off('foo'); + expect(events.get('foo')).to.deep.equal([]); + expect(events.get('bar')).to.have.length(1); + inst.off('bar'); + expect(events.get('bar')).to.deep.equal([]); + }); + }); + + describe('emit()', () => { + it('should be a function', () => { + expect(inst).to.have.property('emit').that.is.a('function'); + }); + + it('should invoke handler for type', () => { + const event = { a: 'b' }; + + inst.on('foo', (one, two?: unknown) => { + expect(one).to.deep.equal(event); + expect(two).to.be.an('undefined'); + }); + + inst.emit('foo', event); + }); + + it('should NOT ignore case', () => { + const onFoo = spy(), + onFOO = spy(); + events.set('Foo', [onFoo]); + events.set('FOO', [onFOO]); + + inst.emit('Foo', 'Foo arg'); + inst.emit('FOO', 'FOO arg'); + + expect(onFoo).to.have.been.calledOnce.and.calledWith('Foo arg'); + expect(onFOO).to.have.been.calledOnce.and.calledWith('FOO arg'); + }); + + it('should invoke * handlers', () => { + const star = spy(), + ea = { a: 'a' }, + eb = { b: 'b' }; + + events.set('*', [star]); + + inst.emit('foo', ea); + expect(star).to.have.been.calledOnce.and.calledWith('foo', ea); + star.resetHistory(); + + inst.emit('bar', eb); + expect(star).to.have.been.calledOnce.and.calledWith('bar', eb); + }); + }); +}); diff --git a/test/test-types-compilation.ts b/test/test-types-compilation.ts new file mode 100644 index 0000000..25c51cc --- /dev/null +++ b/test/test-types-compilation.ts @@ -0,0 +1,78 @@ +/* eslint-disable @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unused-vars */ + +import nitt from '..'; + +interface SomeEventData { + name: string; +} + +const emitter = nitt<{ + foo: string; + someEvent: SomeEventData; + bar?: number; +}>(); + +const barHandler = (x?: number) => {}; +const fooHandler = (x: string) => {}; +const wildcardHandler = ( + _type: 'foo' | 'bar' | 'someEvent', + _event: string | SomeEventData | number | undefined +) => {}; + +/* + * Check that 'on' args are inferred correctly + */ +{ + // @ts-expect-error + emitter.on('foo', barHandler); + emitter.on('foo', fooHandler); + + emitter.on('bar', barHandler); + // @ts-expect-error + emitter.on('bar', fooHandler); + + emitter.on('*', wildcardHandler); + // fooHandler is ok, because ('foo' | 'bar' | 'someEvent') extends string + emitter.on('*', fooHandler); + // @ts-expect-error + emitter.on('*', barHandler); +} + +/* + * Check that 'off' args are inferred correctly + */ +{ + // @ts-expect-error + emitter.off('foo', barHandler); + emitter.off('foo', fooHandler); + + emitter.off('bar', barHandler); + // @ts-expect-error + emitter.off('bar', fooHandler); + + emitter.off('*', wildcardHandler); + // fooHandler is ok, because ('foo' | 'bar' | 'someEvent') extends string + emitter.off('*', fooHandler); + // @ts-expect-error + emitter.off('*', barHandler); +} + +/* + * Check that 'emit' args are inferred correctly + */ +{ + // @ts-expect-error + emitter.emit('someEvent', 'NOT VALID'); + emitter.emit('someEvent', { name: 'jack' }); + + // @ts-expect-error + emitter.emit('foo'); + // @ts-expect-error + emitter.emit('foo', 1); + emitter.emit('foo', 'string'); + + emitter.emit('bar'); + emitter.emit('bar', 1); + // @ts-expect-error + emitter.emit('bar', 'string'); +} diff --git a/tsconfig.json b/tsconfig.json index 8f3c656..8c3bc08 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,16 @@ { - "compilerOptions": { - "lib": ["es2015"] - } + "compileOnSave": false, + "compilerOptions": { + "lib": ["es2015"], + "strict": true, + "noEmit": true, + "declaration": true, + "moduleResolution": "node", + "skipLibCheck": true, + "esModuleInterop": true, + }, + "include": [ + "src/*.ts", + "test/*.ts" + ] } |
