From f6972b427b1ff960380523d0ad3ad957fd4fa2f0 Mon Sep 17 00:00:00 2001 From: Damien Lebrun Date: Mon, 4 Dec 2017 01:50:30 +0000 Subject: [PATCH 01/18] Add TypeScript definition MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It including declarations in the npm package using default declaration locations. Note that by default it doesn’t allow to map an event data type to each event; in TS, Emittery can be aliased to interface to support it. As of TS 2.6, that optional interface doesn’t support symbol. This PR includes some rather slow tests. To skip those tests, ava should exclude test ending with ‘[slow]’, i.e. `ava —match ‘!*[slow]’` Fix #13 --- .gitignore | 1 + examples/.gitignore | 1 + examples/clock.js | 79 +++++++++++++++ examples/clocktyped.ts | 98 ++++++++++++++++++ examples/emit.js | 18 ++++ examples/emitonce.js | 20 ++++ examples/eventdata.js | 20 ++++ index.d.ts | 143 +++++++++++++++++++++++++++ package.json | 14 ++- test/fixtures/compiles/emit.ts | 6 ++ test/fixtures/compiles/off.ts | 9 ++ test/fixtures/compiles/on.ts | 13 +++ test/fixtures/compiles/tsconfig.json | 11 +++ test/fixtures/fails/emit-extra.ts | 6 ++ test/fixtures/fails/on-extra.ts | 6 ++ test/fixtures/tsconfig.json | 11 +++ test/snapshots/types.js.md | 32 ++++++ test/snapshots/types.js.snap | Bin 0 -> 385 bytes test/types.js | 74 ++++++++++++++ tsconfig.json | 13 +++ 20 files changed, 571 insertions(+), 4 deletions(-) create mode 100644 examples/.gitignore create mode 100755 examples/clock.js create mode 100755 examples/clocktyped.ts create mode 100755 examples/emit.js create mode 100755 examples/emitonce.js create mode 100755 examples/eventdata.js create mode 100644 index.d.ts create mode 100644 test/fixtures/compiles/emit.ts create mode 100644 test/fixtures/compiles/off.ts create mode 100644 test/fixtures/compiles/on.ts create mode 100644 test/fixtures/compiles/tsconfig.json create mode 100644 test/fixtures/fails/emit-extra.ts create mode 100644 test/fixtures/fails/on-extra.ts create mode 100644 test/fixtures/tsconfig.json create mode 100644 test/snapshots/types.js.md create mode 100644 test/snapshots/types.js.snap create mode 100644 test/types.js create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index e650ce4..754e1f6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ yarn.lock .nyc_output coverage /legacy.js +/legacy.d.ts diff --git a/examples/.gitignore b/examples/.gitignore new file mode 100644 index 0000000..ed1afdd --- /dev/null +++ b/examples/.gitignore @@ -0,0 +1 @@ +/clocktyped.js diff --git a/examples/clock.js b/examples/clock.js new file mode 100755 index 0000000..a50a56f --- /dev/null +++ b/examples/clock.js @@ -0,0 +1,79 @@ +#!/usr/bin/env node + +'use strict'; + +const Emittery = require('../'); + +class Clock extends Emittery { + + constructor(tick = 1000) { + super(); + + this._startedAt = 0; + this._tick = tick > 0 ? tick : 1000; + this._timer = null; + } + + async tick() { + if (this._timer === null) { + await this.emit('error', new Error('not started')); + this.stop(); + return; + } + + const now = Date.now(); + const duration = now - this._startedAt; + + return this.emit('tick', {now, duration}); + } + + start() { + this._startedAt = Date.now(); + this._timer = setInterval(this.tick.bind(this), this._tick); + this.emit('started', null); + } + + stop() { + if (this._timer !== null) { + clearInterval(this._timer); + } + + this._timer = null; + this._startedAt = 0; + this.emit('stopped', null); + } +} + +const timer = new Clock(); +const offTick = timer.on('tick', onTick); +const offError = timer.on('error', onError); + +timer.start(); + +function onTick({duration}) { + console.log(Math.floor(duration / 1000)); + + if (duration > 5999) { + stop(); + } +} + +function onError(err) { + stop(); + console.error(err); + process.exit(1); +} + +function stop() { + offTick(); + offError(); + timer.stop(); +} + +// Prints: +// 1 +// 2 +// 3 +// 4 +// 5 +// 6 diff --git a/examples/clocktyped.ts b/examples/clocktyped.ts new file mode 100755 index 0000000..ab83cb1 --- /dev/null +++ b/examples/clocktyped.ts @@ -0,0 +1,98 @@ +#!/usr/bin/env ts-node +import _Emittery = require('../'); + +// Alias Emittery class to use mapped event types +const Emittery = _Emittery as _Emittery.IMappedCtor; + +interface ITickData { + now: number; + duration: number; +} + +// Define Clock's events and the data they emit. +type ClockEvents = { + tick: ITickData, + error: Error, + stopped: null, + started: null +}; + +class Clock extends Emittery { + + private _tick: number; + private _timer: NodeJS.Timer | null; + private _startedAt = 0; + + constructor(tick = 1000) { + super(); + + this._tick = tick > 0 ? tick : 1000; + this._timer = null; + } + + async tick(): Promise { + if (this._timer == null) { + await this.emit('error', new Error('not started')); + this.stop(); + return; + } + + const now = Date.now(); + const duration = now - this._startedAt; + + return this.emit('tick', {now, duration}); + } + + start() { + this._startedAt = Date.now(); + this._timer = setInterval(this.tick.bind(this), this._tick); + + this.emit('started', null); + } + + stop() { + if (this._timer != null) { + clearInterval(this._timer); + } + + this._timer = null; + this._startedAt = 0; + + this.emit('stopped', null); + } + +} + +const timer = new Clock(); +const offTick = timer.on('tick', onTick); +const offError = timer.on('error', onError); + +timer.start(); + +function onTick({duration}: ITickData) { + console.log(Math.floor(duration/1000)); + + if (duration > 5999) { + stop(); + } +} + +function onError(err: Error) { + stop(); + console.error(err); + process.exit(1); +} + +function stop() { + offTick(); + offError(); + timer.stop(); +} + +// Prints: +// 1 +// 2 +// 3 +// 4 +// 5 +// 6 diff --git a/examples/emit.js b/examples/emit.js new file mode 100755 index 0000000..1cd8ac7 --- /dev/null +++ b/examples/emit.js @@ -0,0 +1,18 @@ +#!/usr/bin/env node + +'use strict'; + +const Emittery = require('../'); + +const myEmitter = new Emittery(); + +// Emit event in next tick +myEmitter.emit('event'); + +// Register listener +myEmitter.on('event', () => console.log('an event occurred!')); +myEmitter.onAny(eventName => console.log('"%s" event occurred!', eventName)); + +// Prints: +// an event occurred! +// "event" event occurred! diff --git a/examples/emitonce.js b/examples/emitonce.js new file mode 100755 index 0000000..e2c2b90 --- /dev/null +++ b/examples/emitonce.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node + +'use strict'; + +const Emittery = require('../'); + +class MyEmitter extends Emittery {} + +const myEmitter = new MyEmitter(); + +// Emit events in next tick +myEmitter.emit('event', 1); +myEmitter.emit('event', 2); + +// Register listener for only the one event +myEmitter.once('event') + .then(count => console.log('an event occurred (#%d).', count)); + +// Prints: +// an event occurred (#1). diff --git a/examples/eventdata.js b/examples/eventdata.js new file mode 100755 index 0000000..3c32297 --- /dev/null +++ b/examples/eventdata.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node + +'use strict'; + +const Emittery = require('../'); + +class MyEmitter extends Emittery {} + +const myEmitter = new MyEmitter(); + +// Only accept one event data parameter +myEmitter.emit('event', {a: true, b: true}, 'not', 'supported'); + +// Does not provide a context either. +myEmitter.on('event', function ({a, b}, ...args) { + console.log(a, b, args, this); +}); + +// Prints: +// true true [] undefined diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..e2db600 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,143 @@ +// Type definitions for emittery +// Project: emittery +// Definitions by: Sindre Sorhus + +export = Emittery; + +/** + * Async event emitter. + */ +declare class Emittery { + + /** + * Subscribe to an event. + * + * Using the same listener multiple times for the same event will result + * in only one method call per emitted event. + * + * @returns Unsubscribe method. + */ + on(eventName: string, listener: (eventData?: any) => any): () => void; + + /** + * Subscribe to an event only once. It will be unsubscribed after the first + * event. + * + * @returns Promise for the event data when eventName is emitted + */ + once(eventName: string): Promise; + + /** + * Unsubscribe to an event. + * + * If you don't pass in a listener, it will remove all listeners for that + * event. + * + * @param [listener] + */ + off(eventName: string, listener?: (eventData?: any) => any): void; + + /** + * Subscribe to be notified about any event. + * + * @returns A method to unsubscribe + */ + onAny(listener: (eventName: string, eventData?: any) => any): () => void; + + /** + * Unsubscribe an onAny listener. + * + * If you don't pass in a listener, it will remove all onAny listeners. + * + * @param [listener] + */ + offAny(listener?: (eventName: string, eventData?: any) => any): void; + + /** + * Trigger an event asynchronously, optionally with some data. Listeners + * are called in the order they were added, but execute concurrently. + * + * Returns a promise for when all the event listeners are done. Done meaning + * executed if synchronous or resolved when an async/promise-returning + * function. You usually wouldn't want to wait for this, but you could for + * example catch possible errors. If any of the listeners throw/reject, the + * returned promise will be rejected with the error, but the other listeners + * will not be affected. + * + * @returns A promise for when all the event listeners are done. + */ + emit(eventName: string, eventData?: any): Promise; + + /** + * Same as `emit`, but it waits for each listener to resolve before + * triggering the next one. This can be useful if your events depend on each + * other. Although ideally they should not. Prefer emit() whenever possible. + * + * If any of the listeners throw/reject, the returned promise will be + * rejected with the error and the remaining listeners will not be called. + * + * @returns A promise for the last event listener settle or first one rejecting. + */ + emitSerial(eventName: string, eventData?: any): Promise; + + /** + * Clear all event listeners on the instance. + */ + clear(): void; + + /** + * Count event listeners for the eventName or all events if not specified. + * + * @param eventName + * @returns Listener count. + */ + listenerCount(eventName?: string): number; +} + +declare namespace Emittery { + + /** + * A map of event names to the data type they emit. + */ + interface IEvents { + [eventName: string]: any; + } + + /** + * Alternative interface for an Emittery object; must list supported events + * the data type they emit. + */ + export interface IMapped { + on(eventName: Name, listener: (eventData: Events[Name]) => any): () => void; + once(eventName: Name): Promise; + off(eventName: Name, listener?: (eventData: Events[Name]) => any): void; + + onAny(listener: (eventName: Name, eventData: Events[Name]) => any): () => void; + offAny(listener?: (eventName: Name, eventData: Events[Name]) => any): void; + + emit(eventName: Name, eventData: Events[Name]): Promise; + emitSerial(eventName: Name, eventData: Events[Name]): Promise; + } + + /** + * Alternative signature for the Emittery class; must list supported events + * the data type they emit. + * + * @example + * ```ts + * import _Emittery = require('emittery'); + * + * // Alias Emittery class with Events map generic + * const Emittery = _Emittery as _Emittery.IMappedCtor; + * const ee = new Emittery<{open: null, value: string, close: null}>(); + * + * ee.emit('open', null); + * ee.emit('value', 'foo\n'); + * ee.emit('end', null); // TS emit error + * ``` + */ + interface IMappedCtor { + new (): IMapped + } + +} diff --git a/package.json b/package.json index c3d6b39..719ecaf 100644 --- a/package.json +++ b/package.json @@ -13,14 +13,17 @@ "node": ">=4" }, "scripts": { - "build": "babel --out-file=legacy.js index.js", - "build:watch": "npm run build -- --watch", + "build": "npm run build:legacy && cp index.d.ts legacy.d.ts", + "build:legacy": "babel --out-file=legacy.js index.js", + "build:watch": "npm run build:legacy -- --watch", "prepublish": "npm run build", - "test": "xo && nyc ava" + "test": "xo && tsc --noEmit && nyc ava" }, "files": [ "index.js", - "legacy.js" + "index.d.ts", + "legacy.js", + "legacy.d.ts" ], "keywords": [ "event", @@ -47,6 +50,7 @@ "promise" ], "devDependencies": { + "@types/node": "^8.0.54", "ava": "*", "babel-cli": "^6.26.0", "babel-core": "^6.26.0", @@ -55,6 +59,8 @@ "codecov": "^3.0.0", "delay": "^2.0.0", "nyc": "^11.3.0", + "pkg-dir": "^2.0.0", + "typescript": "^2.6.2", "xo": "*" }, "babel": { diff --git a/test/fixtures/compiles/emit.ts b/test/fixtures/compiles/emit.ts new file mode 100644 index 0000000..60a75e0 --- /dev/null +++ b/test/fixtures/compiles/emit.ts @@ -0,0 +1,6 @@ +import Emittery = require('../../..'); + +const ee = new Emittery(); + +ee.emit('anEvent'); +ee.emit('anEvent', 'some data'); diff --git a/test/fixtures/compiles/off.ts b/test/fixtures/compiles/off.ts new file mode 100644 index 0000000..e584620 --- /dev/null +++ b/test/fixtures/compiles/off.ts @@ -0,0 +1,9 @@ +import Emittery = require('../../..'); + +const ee = new Emittery(); + +ee.off('anEvent', () => undefined); +ee.off('anEvent', () => Promise.resolve()); + +ee.off('anEvent', data => undefined); +ee.off('anEvent', data => Promise.resolve()); diff --git a/test/fixtures/compiles/on.ts b/test/fixtures/compiles/on.ts new file mode 100644 index 0000000..97cfcef --- /dev/null +++ b/test/fixtures/compiles/on.ts @@ -0,0 +1,13 @@ +import Emittery = require('../../..'); + +const ee = new Emittery(); + +ee.on('anEvent', () => undefined); +ee.on('anEvent', () => Promise.resolve()); + +ee.on('anEvent', data => undefined); +ee.on('anEvent', data => Promise.resolve()); + +const off = ee.on('anEvent', () => undefined); + +off(); diff --git a/test/fixtures/compiles/tsconfig.json b/test/fixtures/compiles/tsconfig.json new file mode 100644 index 0000000..27a6731 --- /dev/null +++ b/test/fixtures/compiles/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2017", + "lib": ["es2017"], + "module": "commonjs", + "strict": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true + }, + "include": ["*.ts"] +} diff --git a/test/fixtures/fails/emit-extra.ts b/test/fixtures/fails/emit-extra.ts new file mode 100644 index 0000000..4eb97c6 --- /dev/null +++ b/test/fixtures/fails/emit-extra.ts @@ -0,0 +1,6 @@ +import Emittery = require('../../..'); + +const ee = new Emittery(); + +ee.emit('anEvent'); +ee.emit('anEvent', 'some data', 'and more'); diff --git a/test/fixtures/fails/on-extra.ts b/test/fixtures/fails/on-extra.ts new file mode 100644 index 0000000..6a90bbe --- /dev/null +++ b/test/fixtures/fails/on-extra.ts @@ -0,0 +1,6 @@ +import Emittery = require('../../..'); + +const ee = new Emittery(); + +ee.on('anEvent', (data, more) => undefined); + diff --git a/test/fixtures/tsconfig.json b/test/fixtures/tsconfig.json new file mode 100644 index 0000000..27a6731 --- /dev/null +++ b/test/fixtures/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "es2017", + "lib": ["es2017"], + "module": "commonjs", + "strict": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true + }, + "include": ["*.ts"] +} diff --git a/test/snapshots/types.js.md b/test/snapshots/types.js.md new file mode 100644 index 0000000..ea5c7e6 --- /dev/null +++ b/test/snapshots/types.js.md @@ -0,0 +1,32 @@ +# Snapshot report for `test/types.js` + +The actual snapshot is saved in `types.js.snap`. + +Generated by [AVA](https://ava.li). + +## Type definition disallows invalid Emittery method calls to compile + +> Snapshot 1 + + `test/fixtures/fails/emit-extra.ts (6,1): Expected 1-2 arguments, but got 3.␊ + test/fixtures/fails/on-extra.ts (5,18): Argument of type '(data: any, more: any) => undefined' is not assignable to parameter of type '() => any'.␊ + test/fixtures/fails/on-extra.ts (5,19): Parameter 'data' implicitly has an 'any' type.␊ + test/fixtures/fails/on-extra.ts (5,25): Parameter 'more' implicitly has an 'any' type. + +## Type definition disallows invalid Emittery method calls to compile [slow] + +> Snapshot 1 + + `test/fixtures/fails/emit-extra.ts (6,1): Expected 1-2 arguments, but got 3.␊ + test/fixtures/fails/on-extra.ts (5,18): Argument of type '(data: any, more: any) => undefined' is not assignable to parameter of type '() => any'.␊ + test/fixtures/fails/on-extra.ts (5,19): Parameter 'data' implicitly has an 'any' type.␊ + test/fixtures/fails/on-extra.ts (5,25): Parameter 'more' implicitly has an 'any' type. + +## TS warns about invalid Emittery method calls [slow] + +> Snapshot 1 + + `test/fixtures/fails/emit-extra.ts (6,1): Expected 1-2 arguments, but got 3.␊ + test/fixtures/fails/on-extra.ts (5,18): Argument of type '(data: any, more: any) => undefined' is not assignable to parameter of type '(eventData?: any) => any'.␊ + test/fixtures/fails/on-extra.ts (5,19): Parameter 'data' implicitly has an 'any' type.␊ + test/fixtures/fails/on-extra.ts (5,25): Parameter 'more' implicitly has an 'any' type.` \ No newline at end of file diff --git a/test/snapshots/types.js.snap b/test/snapshots/types.js.snap new file mode 100644 index 0000000000000000000000000000000000000000..e7adc5e0395edd79b87bdc453f9003ee3cf9cdf4 GIT binary patch literal 385 zcmV-{0e=2LRzVDW#J_hJ<(uNRR4{@?AFwbmFfp)$^)s?C2r>0AmZTP!=%-~?l#~{w7VD=a zX66*@r{-ps=%!Yb6ea4F6f0<$=@@ESDY#Y?q$Zc7rYIQd8Yv_erI+TW=9LudC?u7Z zD5U3?C>ZN;VKXZ~56L7`9YYJCQI1gE3i)XYC6xuK3hEjui6x0v3W<4@Itsb@MX6w> zrh=`VLTO$~YFcJqYKpo-X0bvZ(4xfR;>`5C#H5^5g_3-Qg2bZ4+|-iPA|%7W8i59= z6XzsLppydN#;Ajw0<X&jifSy#TLg?9jhdkm fHM56h)TEXH4R-;WXpb;w2*wlu=bysJGz9 (err.file ? err.file.fileName : null)) + .filter(Boolean); + + t.deepEqual(new Set(filesWithErrors), new Set(fileNames), 'Some files did not emit any compile error.'); + t.snapshot(errorMessage(errors)); +} + +function listFiles(srcRoot) { + return readdirSync(srcRoot) + .filter(hasValidExtension) + .map(name => join(srcRoot, name)) + .filter(isFile); +} + +function compile(fileNames, options = compilerOptions) { + const program = ts.createProgram(fileNames, options); + const emitResult = program.emit(); + + return ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics); +} + +function errorMessage(diagnosticList) { + const root = pkgUp.sync(); + + return diagnosticList.map(diagnostic => { + if (!diagnostic.file) { + return `${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`; + } + + const {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); + const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); + const fileName = relative(root, diagnostic.file.fileName); + + return `${fileName} (${line + 1},${character + 1}): ${message}`; + }).join('\n'); +} + +function isFile(path) { + return statSync(path).isFile(); +} + +function hasValidExtension(path) { + return validExtension.has(extname(path)); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ada0846 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2017", + "lib": ["es2017"], + "module": "commonjs", + "strict": true, + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "allowJs": true + }, + "include": ["**/*.ts"], + "exclude": ["test/fixtures/**/*"] +} From f39dbcdf22e5c78cee9e6418691ca6fddbe691bb Mon Sep 17 00:00:00 2001 From: Damien Lebrun Date: Sat, 16 Dec 2017 00:52:12 +0000 Subject: [PATCH 02/18] fixup! Add TypeScript definition --- examples/clocktyped.ts | 17 +++++++++-------- index.d.ts | 29 ++++++++++++++++++++--------- 2 files changed, 29 insertions(+), 17 deletions(-) diff --git a/examples/clocktyped.ts b/examples/clocktyped.ts index ab83cb1..11595b1 100755 --- a/examples/clocktyped.ts +++ b/examples/clocktyped.ts @@ -9,15 +9,16 @@ interface ITickData { duration: number; } -// Define Clock's events and the data they emit. -type ClockEvents = { +// Map Clock's events emitting data to the type of their data. +type EventDataMap = { tick: ITickData, - error: Error, - stopped: null, - started: null + error: Error }; -class Clock extends Emittery { +// List of event which do not required data +type EmptyEvents = 'started' | 'stopped'; + +class Clock extends Emittery { private _tick: number; private _timer: NodeJS.Timer | null; @@ -47,7 +48,7 @@ class Clock extends Emittery { this._startedAt = Date.now(); this._timer = setInterval(this.tick.bind(this), this._tick); - this.emit('started', null); + this.emit('started'); } stop() { @@ -58,7 +59,7 @@ class Clock extends Emittery { this._timer = null; this._startedAt = 0; - this.emit('stopped', null); + this.emit('stopped'); } } diff --git a/index.d.ts b/index.d.ts index e2db600..515bbd0 100644 --- a/index.d.ts +++ b/index.d.ts @@ -107,16 +107,27 @@ declare namespace Emittery { * Alternative interface for an Emittery object; must list supported events * the data type they emit. */ - export interface IMapped { - on(eventName: Name, listener: (eventData: Events[Name]) => any): () => void; - once(eventName: Name): Promise; - off(eventName: Name, listener?: (eventData: Events[Name]) => any): void; + export interface IMapped { + on(eventName: Name, listener: (eventData: EventDataMap[Name]) => any): () => void; + on(eventName: Name, listener: () => any): () => void; - onAny(listener: (eventName: Name, eventData: Events[Name]) => any): () => void; - offAny(listener?: (eventName: Name, eventData: Events[Name]) => any): void; + once(eventName: Name): Promise; + once(eventName: Name): Promise; - emit(eventName: Name, eventData: Events[Name]): Promise; - emitSerial(eventName: Name, eventData: Events[Name]): Promise; + off(eventName: Name, listener?: (eventData: EventDataMap[Name]) => any): void; + off(eventName: Name, listener?: () => any): void; + + onAny(listener: (eventName: Name, eventData: EventDataMap[Name]) => any): () => void; + onAny(listener: (eventName: Name) => any): () => void; + + offAny(listener?: (eventName: Name, eventData: EventDataMap[Name]) => any): void; + offAny(listener?: (eventName: Name) => any): void; + + emit(eventName: Name, eventData: EventDataMap[Name]): Promise; + emit(eventName: Name): Promise; + + emitSerial(eventName: Name, eventData: EventDataMap[Name]): Promise; + emitSerial(eventName: Name): Promise; } /** @@ -137,7 +148,7 @@ declare namespace Emittery { * ``` */ interface IMappedCtor { - new (): IMapped + new (): IMapped } } From 88c1fa4883d89a6b9675e0f0783f441c8f4fec64 Mon Sep 17 00:00:00 2001 From: Damien Lebrun Date: Sat, 16 Dec 2017 00:52:34 +0000 Subject: [PATCH 03/18] fixup! Add TypeScript definition --- examples/clocktyped.ts | 8 ++++---- index.d.ts | 17 +++++++++-------- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/examples/clocktyped.ts b/examples/clocktyped.ts index 11595b1..a243a2c 100755 --- a/examples/clocktyped.ts +++ b/examples/clocktyped.ts @@ -2,16 +2,16 @@ import _Emittery = require('../'); // Alias Emittery class to use mapped event types -const Emittery = _Emittery as _Emittery.IMappedCtor; +const Emittery = _Emittery as _Emittery.MappedCtor; -interface ITickData { +interface TickData { now: number; duration: number; } // Map Clock's events emitting data to the type of their data. type EventDataMap = { - tick: ITickData, + tick: TickData, error: Error }; @@ -70,7 +70,7 @@ const offError = timer.on('error', onError); timer.start(); -function onTick({duration}: ITickData) { +function onTick({duration}: TickData) { console.log(Math.floor(duration/1000)); if (duration > 5999) { diff --git a/index.d.ts b/index.d.ts index 515bbd0..c15d02d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -99,7 +99,7 @@ declare namespace Emittery { /** * A map of event names to the data type they emit. */ - interface IEvents { + interface Events { [eventName: string]: any; } @@ -107,7 +107,7 @@ declare namespace Emittery { * Alternative interface for an Emittery object; must list supported events * the data type they emit. */ - export interface IMapped { + interface Mapped { on(eventName: Name, listener: (eventData: EventDataMap[Name]) => any): () => void; on(eventName: Name, listener: () => any): () => void; @@ -139,16 +139,17 @@ declare namespace Emittery { * import _Emittery = require('emittery'); * * // Alias Emittery class with Events map generic - * const Emittery = _Emittery as _Emittery.IMappedCtor; - * const ee = new Emittery<{open: null, value: string, close: null}>(); + * const Emittery = _Emittery as _Emittery.MappedCtor; + * const ee = new Emittery<{value: string}, 'open' | 'close'>(); * - * ee.emit('open', null); + * ee.emit('open'); * ee.emit('value', 'foo\n'); - * ee.emit('end', null); // TS emit error + * ee.emit('value', 1); // TS emit error + * ee.emit('end'); // TS emit error * ``` */ - interface IMappedCtor { - new (): IMapped + interface MappedCtor { + new (): Mapped } } From 1092de2f919271bec0653582e553a09f4c116ccc Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Tue, 2 Jan 2018 14:58:55 +0000 Subject: [PATCH 04/18] Alias Emittery definition for legacy module Rather than copying the definition file, re-export it from a committed `legacy.d.ts`. Rename `index.d.ts` so the definition can be imported. --- .gitignore | 1 - index.d.ts => Emittery.d.ts | 0 legacy.d.ts | 2 ++ package.json | 8 ++++---- 4 files changed, 6 insertions(+), 5 deletions(-) rename index.d.ts => Emittery.d.ts (100%) create mode 100644 legacy.d.ts diff --git a/.gitignore b/.gitignore index 754e1f6..e650ce4 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,3 @@ yarn.lock .nyc_output coverage /legacy.js -/legacy.d.ts diff --git a/index.d.ts b/Emittery.d.ts similarity index 100% rename from index.d.ts rename to Emittery.d.ts diff --git a/legacy.d.ts b/legacy.d.ts new file mode 100644 index 0000000..2e6f5f5 --- /dev/null +++ b/legacy.d.ts @@ -0,0 +1,2 @@ +import Emittery = require('./Emittery') +export = Emittery diff --git a/package.json b/package.json index 719ecaf..496b8bb 100644 --- a/package.json +++ b/package.json @@ -13,18 +13,18 @@ "node": ">=4" }, "scripts": { - "build": "npm run build:legacy && cp index.d.ts legacy.d.ts", - "build:legacy": "babel --out-file=legacy.js index.js", - "build:watch": "npm run build:legacy -- --watch", + "build": "babel --out-file=legacy.js index.js", + "build:watch": "npm run build -- --watch", "prepublish": "npm run build", "test": "xo && tsc --noEmit && nyc ava" }, "files": [ + "Emittery.d.ts", "index.js", - "index.d.ts", "legacy.js", "legacy.d.ts" ], + "typings": "./Emittery.d.ts", "keywords": [ "event", "emitter", From ad891fae807146498376bdb417fd80ff32becaae Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Tue, 2 Jan 2018 15:02:53 +0000 Subject: [PATCH 05/18] Use individual strictness compiler options --strict means future minor releases may end up working stricter in a non-backwards compatible manner. --- tsconfig.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index ada0846..b153639 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,7 +3,10 @@ "target": "es2017", "lib": ["es2017"], "module": "commonjs", - "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strictFunctionTypes": true, + "strictNullChecks": true, "moduleResolution": "node", "allowSyntheticDefaultImports": true, "allowJs": true From 445c12e5da5671e108f3940f92fe448cea7feb0b Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Tue, 2 Jan 2018 15:48:07 +0000 Subject: [PATCH 06/18] Clean up file inclusion * allowJs was superfluous with the explicit include option * The include option matches the default behavior (without allowJs) * The exclude option can be simplified --- tsconfig.json | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index b153639..452103b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,9 +8,7 @@ "strictFunctionTypes": true, "strictNullChecks": true, "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "allowJs": true + "allowSyntheticDefaultImports": true }, - "include": ["**/*.ts"], - "exclude": ["test/fixtures/**/*"] + "exclude": ["test/fixtures"] } From 0cbb56ff57877d4d2277cb503d8c20f01e5342d8 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Tue, 2 Jan 2018 15:49:24 +0000 Subject: [PATCH 07/18] Add additional compiler options --- tsconfig.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tsconfig.json b/tsconfig.json index 452103b..f5cb311 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,11 @@ { "compilerOptions": { + "alwaysStrict": true, "target": "es2017", "lib": ["es2017"], "module": "commonjs", "noImplicitAny": true, + "noImplicitReturns": true, "noImplicitThis": true, "strictFunctionTypes": true, "strictNullChecks": true, From 73fb721967d6008afc57fb95161e14da3a86d6e4 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Tue, 2 Jan 2018 15:49:58 +0000 Subject: [PATCH 08/18] Reformat tsconfig.json --- tsconfig.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index f5cb311..21b3bd7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,16 @@ { "compilerOptions": { + "allowSyntheticDefaultImports": true, "alwaysStrict": true, - "target": "es2017", "lib": ["es2017"], "module": "commonjs", + "moduleResolution": "node", "noImplicitAny": true, "noImplicitReturns": true, "noImplicitThis": true, "strictFunctionTypes": true, "strictNullChecks": true, - "moduleResolution": "node", - "allowSyntheticDefaultImports": true + "target": "es2017" }, "exclude": ["test/fixtures"] } From aff3fc4c0253315a2aba3b881c970a7b1c22484b Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Tue, 2 Jan 2018 16:04:04 +0000 Subject: [PATCH 09/18] Update @types/node --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 496b8bb..91a9688 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "promise" ], "devDependencies": { - "@types/node": "^8.0.54", + "@types/node": "^8.5.2", "ava": "*", "babel-cli": "^6.26.0", "babel-core": "^6.26.0", From 86109198bacfe9f26dc6a1ac14610d47a71216ab Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Tue, 2 Jan 2018 16:13:53 +0000 Subject: [PATCH 10/18] Install ts-node and select it through npx in TS example --- examples/clocktyped.ts | 2 +- package.json | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/clocktyped.ts b/examples/clocktyped.ts index a243a2c..1f16fd5 100755 --- a/examples/clocktyped.ts +++ b/examples/clocktyped.ts @@ -1,4 +1,4 @@ -#!/usr/bin/env ts-node +#!/usr/bin/env npx ts-node import _Emittery = require('../'); // Alias Emittery class to use mapped event types diff --git a/package.json b/package.json index 91a9688..d0fa856 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "delay": "^2.0.0", "nyc": "^11.3.0", "pkg-dir": "^2.0.0", + "ts-node": "^4.1.0", "typescript": "^2.6.2", "xo": "*" }, From 96125135a09b841c362afa2823ea491439eaef90 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Tue, 2 Jan 2018 16:37:11 +0000 Subject: [PATCH 11/18] Clean up clock examples --- examples/clock.js | 62 ++++++++++++++++++------------------ examples/clocktyped.ts | 71 ++++++++++++++++++++---------------------- 2 files changed, 65 insertions(+), 68 deletions(-) diff --git a/examples/clock.js b/examples/clock.js index a50a56f..79d1cc3 100755 --- a/examples/clock.js +++ b/examples/clock.js @@ -1,75 +1,75 @@ #!/usr/bin/env node - 'use strict'; -const Emittery = require('../'); +const Emittery = require('..'); class Clock extends Emittery { - - constructor(tick = 1000) { + constructor() { super(); - - this._startedAt = 0; - this._tick = tick > 0 ? tick : 1000; - this._timer = null; + this.startedAt = 0; + this.timer = null; } - async tick() { - if (this._timer === null) { - await this.emit('error', new Error('not started')); - this.stop(); + tick() { + if (!this.timer) { + this.emit('error', new Error('Clock has not been started')); return; } const now = Date.now(); - const duration = now - this._startedAt; + const duration = now - this.startedAt; - return this.emit('tick', {now, duration}); + this.emit('tick', {duration, now}); } start() { - this._startedAt = Date.now(); - this._timer = setInterval(this.tick.bind(this), this._tick); - this.emit('started', null); + if (this.timer) { + throw new Error('Clock has already been started'); + } + + this.startedAt = Date.now(); + this.timer = setInterval(this.tick.bind(this), 1000); + + this.emit('start'); } stop() { - if (this._timer !== null) { - clearInterval(this._timer); + if (this.timer) { + clearInterval(this.timer); } - this._timer = null; - this._startedAt = 0; - this.emit('stopped', null); + this.startedAt = 0; + this.timer = null; + + this.emit('stop'); } } -const timer = new Clock(); -const offTick = timer.on('tick', onTick); -const offError = timer.on('error', onError); - -timer.start(); - function onTick({duration}) { console.log(Math.floor(duration / 1000)); - if (duration > 5999) { + if (duration >= 6000) { stop(); } } function onError(err) { - stop(); + process.exitCode = 1; console.error(err); - process.exit(1); + stop(); } +const timer = new Clock(); +const offTick = timer.on('tick', onTick); +const offError = timer.on('error', onError); + function stop() { offTick(); offError(); timer.stop(); } +timer.start(); // Prints: // 1 // 2 diff --git a/examples/clocktyped.ts b/examples/clocktyped.ts index 1f16fd5..8b7f804 100755 --- a/examples/clocktyped.ts +++ b/examples/clocktyped.ts @@ -1,5 +1,6 @@ #!/usr/bin/env npx ts-node -import _Emittery = require('../'); +import {setInterval} from 'timers'; +import _Emittery = require('..'); // Alias Emittery class to use mapped event types const Emittery = _Emittery as _Emittery.MappedCtor; @@ -16,80 +17,76 @@ type EventDataMap = { }; // List of event which do not required data -type EmptyEvents = 'started' | 'stopped'; +type EmptyEvents = 'start' | 'stop'; class Clock extends Emittery { + private startedAt = 0; + private timer: NodeJS.Timer | null = null; - private _tick: number; - private _timer: NodeJS.Timer | null; - private _startedAt = 0; - - constructor(tick = 1000) { + public constructor() { super(); - - this._tick = tick > 0 ? tick : 1000; - this._timer = null; } - async tick(): Promise { - if (this._timer == null) { - await this.emit('error', new Error('not started')); - this.stop(); + private tick() { + if (!this.timer) { + this.emit('error', new Error('Clock has not been started')); return; } const now = Date.now(); - const duration = now - this._startedAt; + const duration = now - this.startedAt; - return this.emit('tick', {now, duration}); + this.emit('tick', {duration, now}); } - start() { - this._startedAt = Date.now(); - this._timer = setInterval(this.tick.bind(this), this._tick); + public start() { + if (this.timer) { + throw new Error('Clock has already been started'); + } - this.emit('started'); + this.startedAt = Date.now(); + this.timer = setInterval(this.tick.bind(this), 1000); + + this.emit('start'); } - stop() { - if (this._timer != null) { - clearInterval(this._timer); + public stop() { + if (this.timer) { + clearInterval(this.timer); } - this._timer = null; - this._startedAt = 0; + this.startedAt = 0; + this.timer = null; - this.emit('stopped'); + this.emit('stop'); } - } -const timer = new Clock(); -const offTick = timer.on('tick', onTick); -const offError = timer.on('error', onError); - -timer.start(); - function onTick({duration}: TickData) { - console.log(Math.floor(duration/1000)); + console.log(Math.floor(duration / 1000)); - if (duration > 5999) { + if (duration >= 6000) { stop(); } } function onError(err: Error) { - stop(); + process.exitCode = 1; console.error(err); - process.exit(1); + stop(); } +const timer = new Clock(); +const offTick = timer.on('tick', onTick); +const offError = timer.on('error', onError); + function stop() { offTick(); offError(); timer.stop(); } +timer.start(); // Prints: // 1 // 2 From e042d55d079650b97a8bd089a03780873b291bfa Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Tue, 2 Jan 2018 17:49:07 +0000 Subject: [PATCH 12/18] Improve usability of "mapped" interface Create a `Typed` subclass of `Emittery` onto which we can hang the TypeScript method overloads. --- Emittery.d.ts | 43 +++++++++++++++++------------------------- examples/clocktyped.ts | 7 ++----- index.js | 13 +++++++++++-- 3 files changed, 30 insertions(+), 33 deletions(-) diff --git a/Emittery.d.ts b/Emittery.d.ts index c15d02d..857cd60 100644 --- a/Emittery.d.ts +++ b/Emittery.d.ts @@ -104,10 +104,24 @@ declare namespace Emittery { } /** - * Alternative interface for an Emittery object; must list supported events - * the data type they emit. + * Async event emitter. + * + * Must list supported events and the data type they emit, if any. + * + * For example: + * + * ```ts + * import Emittery = require('emittery'); + * + * const ee = new Emittery.Typed<{value: string}, 'open' | 'close'>(); + * + * ee.emit('open'); + * ee.emit('value', 'foo\n'); + * ee.emit('value', 1); // TS emit error + * ee.emit('end'); // TS emit error + * ``` */ - interface Mapped { + class Typed extends Emittery { on(eventName: Name, listener: (eventData: EventDataMap[Name]) => any): () => void; on(eventName: Name, listener: () => any): () => void; @@ -129,27 +143,4 @@ declare namespace Emittery { emitSerial(eventName: Name, eventData: EventDataMap[Name]): Promise; emitSerial(eventName: Name): Promise; } - - /** - * Alternative signature for the Emittery class; must list supported events - * the data type they emit. - * - * @example - * ```ts - * import _Emittery = require('emittery'); - * - * // Alias Emittery class with Events map generic - * const Emittery = _Emittery as _Emittery.MappedCtor; - * const ee = new Emittery<{value: string}, 'open' | 'close'>(); - * - * ee.emit('open'); - * ee.emit('value', 'foo\n'); - * ee.emit('value', 1); // TS emit error - * ee.emit('end'); // TS emit error - * ``` - */ - interface MappedCtor { - new (): Mapped - } - } diff --git a/examples/clocktyped.ts b/examples/clocktyped.ts index 8b7f804..0cc8484 100755 --- a/examples/clocktyped.ts +++ b/examples/clocktyped.ts @@ -1,9 +1,6 @@ #!/usr/bin/env npx ts-node import {setInterval} from 'timers'; -import _Emittery = require('..'); - -// Alias Emittery class to use mapped event types -const Emittery = _Emittery as _Emittery.MappedCtor; +import Emittery = require('..'); interface TickData { now: number; @@ -19,7 +16,7 @@ type EventDataMap = { // List of event which do not required data type EmptyEvents = 'start' | 'stop'; -class Clock extends Emittery { +class Clock extends Emittery.Typed { private startedAt = 0; private timer: NodeJS.Timer | null = null; diff --git a/index.js b/index.js index 35f07a4..3e5ce9a 100644 --- a/index.js +++ b/index.js @@ -8,7 +8,7 @@ function assertEventName(eventName) { } } -module.exports = class Emittery { +class Emittery { constructor() { this._events = new Map(); this._anyEvents = new Set(); @@ -105,4 +105,13 @@ module.exports = class Emittery { return count; } -}; +} + +// Subclass used to encourage TS users to type their events. +Emittery.Typed = class extends Emittery {}; +Object.defineProperty(Emittery.Typed, 'Typed', { + enumerable: false, + value: undefined +}); + +module.exports = Emittery; From 7a45a09f945361fbbc605203522eea8f49211589 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Tue, 2 Jan 2018 17:52:27 +0000 Subject: [PATCH 13/18] Reword off() and offAny() descriptions I found "unsubscribe to an event" too ambiguous. --- readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 5f4900f..3552a86 100644 --- a/readme.md +++ b/readme.md @@ -51,7 +51,7 @@ Using the same listener multiple times for the same event will result in only on #### off(eventName, [listener]) -Unsubscribe to an event. +Remove an event subscription. If you don't pass in a `listener`, it will remove all listeners for that event. @@ -94,9 +94,9 @@ Returns a method to unsubscribe. #### offAny([listener]) -Unsubscribe an `onAny` listener. +Remove an `onAny` subscription. -If you don't pass in a `listener`, it will remove all `onAny` listeners. +If you don't pass in a `listener`, it will remove all `onAny` subscriptions. #### clear() From 653f92116e72a72457b133c7b82e738f31b9149a Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Tue, 2 Jan 2018 17:53:08 +0000 Subject: [PATCH 14/18] Update TS code comments * Mirror the README * Remove unused JSDoc annotations (as far as VS Code and Atom are concerned) * Alias the unsubscribe function which leads to nicer type hints in VS Code and Atom --- Emittery.d.ts | 105 +++++++++++++++++++++++--------------------------- 1 file changed, 48 insertions(+), 57 deletions(-) diff --git a/Emittery.d.ts b/Emittery.d.ts index 857cd60..9235e18 100644 --- a/Emittery.d.ts +++ b/Emittery.d.ts @@ -1,84 +1,73 @@ -// Type definitions for emittery -// Project: emittery -// Definitions by: Sindre Sorhus - export = Emittery; -/** - * Async event emitter. - */ declare class Emittery { - /** * Subscribe to an event. * + * Returns an unsubscribe method. + * * Using the same listener multiple times for the same event will result * in only one method call per emitted event. - * - * @returns Unsubscribe method. */ - on(eventName: string, listener: (eventData?: any) => any): () => void; + on(eventName: string, listener: (eventData?: any) => any): Emittery.UnsubscribeFn; + + /** + * Remove an event subscription. + * + * If you don't pass in a `listener`, it will remove all listeners for that + * event. + */ + off(eventName: string, listener?: (eventData?: any) => any): void; /** * Subscribe to an event only once. It will be unsubscribed after the first * event. * - * @returns Promise for the event data when eventName is emitted + * Returns a promise for the event data when `eventName` is emitted. */ once(eventName: string): Promise; /** - * Unsubscribe to an event. - * - * If you don't pass in a listener, it will remove all listeners for that - * event. + * Trigger an event asynchronously, optionally with some data. Listeners + * are called in the order they were added, but execute concurrently. * - * @param [listener] - */ - off(eventName: string, listener?: (eventData?: any) => any): void; - - /** - * Subscribe to be notified about any event. + * Returns a promise for when all the event listeners are done. *Done* + * meaning executed if synchronous or resolved when an + * async/promise-returning function. You usually wouldn't want to wait for + * this, but you could for example catch possible errors. If any of the + * listeners throw/reject, the returned promise will be rejected with the + * error, but the other listeners will not be affected. * - * @returns A method to unsubscribe + * Returns a promise for when all the event listeners are done. */ - onAny(listener: (eventName: string, eventData?: any) => any): () => void; + emit(eventName: string, eventData?: any): Promise; /** - * Unsubscribe an onAny listener. + * Same as `emit()`, but it waits for each listener to resolve before + * triggering the next one. This can be useful if your events depend on each + * other. Although ideally they should not. Prefer `emit()` whenever + * possible. * - * If you don't pass in a listener, it will remove all onAny listeners. + * If any of the listeners throw/reject, the returned promise will be + * rejected with the error and the remaining listeners will *not* be called. * - * @param [listener] + * Returns a promise for when all the event listeners are done. */ - offAny(listener?: (eventName: string, eventData?: any) => any): void; + emitSerial(eventName: string, eventData?: any): Promise; /** - * Trigger an event asynchronously, optionally with some data. Listeners - * are called in the order they were added, but execute concurrently. - * - * Returns a promise for when all the event listeners are done. Done meaning - * executed if synchronous or resolved when an async/promise-returning - * function. You usually wouldn't want to wait for this, but you could for - * example catch possible errors. If any of the listeners throw/reject, the - * returned promise will be rejected with the error, but the other listeners - * will not be affected. + * Subscribe to be notified about any event. * - * @returns A promise for when all the event listeners are done. + * Returns a method to unsubscribe. */ - emit(eventName: string, eventData?: any): Promise; + onAny(listener: (eventName: string, eventData?: any) => any): Emittery.UnsubscribeFn; /** - * Same as `emit`, but it waits for each listener to resolve before - * triggering the next one. This can be useful if your events depend on each - * other. Although ideally they should not. Prefer emit() whenever possible. - * - * If any of the listeners throw/reject, the returned promise will be - * rejected with the error and the remaining listeners will not be called. - * - * @returns A promise for the last event listener settle or first one rejecting. + Remove an `onAny` subscription. + + If you don't pass in a `listener`, it will remove all `onAny` subscriptions. */ - emitSerial(eventName: string, eventData?: any): Promise; + offAny(listener?: (eventName: string, eventData?: any) => any): void; /** * Clear all event listeners on the instance. @@ -86,18 +75,20 @@ declare class Emittery { clear(): void; /** - * Count event listeners for the eventName or all events if not specified. - * - * @param eventName - * @returns Listener count. + * The number of listeners for the `eventName` or all events if not + * specified. */ listenerCount(eventName?: string): number; } declare namespace Emittery { + /** + * Removes an event subscription. + */ + type UnsubscribeFn = () => void; /** - * A map of event names to the data type they emit. + * Maps event names to their emitted data type. */ interface Events { [eventName: string]: any; @@ -122,8 +113,8 @@ declare namespace Emittery { * ``` */ class Typed extends Emittery { - on(eventName: Name, listener: (eventData: EventDataMap[Name]) => any): () => void; - on(eventName: Name, listener: () => any): () => void; + on(eventName: Name, listener: (eventData: EventDataMap[Name]) => any): Emittery.UnsubscribeFn; + on(eventName: Name, listener: () => any): Emittery.UnsubscribeFn; once(eventName: Name): Promise; once(eventName: Name): Promise; @@ -131,8 +122,8 @@ declare namespace Emittery { off(eventName: Name, listener?: (eventData: EventDataMap[Name]) => any): void; off(eventName: Name, listener?: () => any): void; - onAny(listener: (eventName: Name, eventData: EventDataMap[Name]) => any): () => void; - onAny(listener: (eventName: Name) => any): () => void; + onAny(listener: (eventName: Name, eventData: EventDataMap[Name]) => any): Emittery.UnsubscribeFn; + onAny(listener: (eventName: Name) => any): Emittery.UnsubscribeFn; offAny(listener?: (eventName: Name, eventData: EventDataMap[Name]) => any): void; offAny(listener?: (eventName: Name) => any): void; From c79d66893346af8832011760b061d9290dd42127 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Tue, 2 Jan 2018 17:57:33 +0000 Subject: [PATCH 15/18] Clean up remaining examples --- examples/emit.js | 2 +- examples/emitonce.js | 6 ++---- examples/eventdata.js | 4 +--- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/examples/emit.js b/examples/emit.js index 1cd8ac7..be1c038 100755 --- a/examples/emit.js +++ b/examples/emit.js @@ -2,7 +2,7 @@ 'use strict'; -const Emittery = require('../'); +const Emittery = require('..'); const myEmitter = new Emittery(); diff --git a/examples/emitonce.js b/examples/emitonce.js index e2c2b90..c036630 100755 --- a/examples/emitonce.js +++ b/examples/emitonce.js @@ -2,11 +2,9 @@ 'use strict'; -const Emittery = require('../'); +const Emittery = require('..'); -class MyEmitter extends Emittery {} - -const myEmitter = new MyEmitter(); +const myEmitter = new Emittery(); // Emit events in next tick myEmitter.emit('event', 1); diff --git a/examples/eventdata.js b/examples/eventdata.js index 3c32297..ed7ea5b 100755 --- a/examples/eventdata.js +++ b/examples/eventdata.js @@ -4,9 +4,7 @@ const Emittery = require('../'); -class MyEmitter extends Emittery {} - -const myEmitter = new MyEmitter(); +const myEmitter = new Emittery(); // Only accept one event data parameter myEmitter.emit('event', {a: true, b: true}, 'not', 'supported'); From 92dd4736bea42c0718f3def3df3305a167564751 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Tue, 2 Jan 2018 18:11:30 +0000 Subject: [PATCH 16/18] Remove unused snapshot --- test/snapshots/types.js.md | 20 +------------------- test/snapshots/types.js.snap | Bin 385 -> 311 bytes 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/test/snapshots/types.js.md b/test/snapshots/types.js.md index ea5c7e6..8b2f9ab 100644 --- a/test/snapshots/types.js.md +++ b/test/snapshots/types.js.md @@ -4,24 +4,6 @@ The actual snapshot is saved in `types.js.snap`. Generated by [AVA](https://ava.li). -## Type definition disallows invalid Emittery method calls to compile - -> Snapshot 1 - - `test/fixtures/fails/emit-extra.ts (6,1): Expected 1-2 arguments, but got 3.␊ - test/fixtures/fails/on-extra.ts (5,18): Argument of type '(data: any, more: any) => undefined' is not assignable to parameter of type '() => any'.␊ - test/fixtures/fails/on-extra.ts (5,19): Parameter 'data' implicitly has an 'any' type.␊ - test/fixtures/fails/on-extra.ts (5,25): Parameter 'more' implicitly has an 'any' type. - -## Type definition disallows invalid Emittery method calls to compile [slow] - -> Snapshot 1 - - `test/fixtures/fails/emit-extra.ts (6,1): Expected 1-2 arguments, but got 3.␊ - test/fixtures/fails/on-extra.ts (5,18): Argument of type '(data: any, more: any) => undefined' is not assignable to parameter of type '() => any'.␊ - test/fixtures/fails/on-extra.ts (5,19): Parameter 'data' implicitly has an 'any' type.␊ - test/fixtures/fails/on-extra.ts (5,25): Parameter 'more' implicitly has an 'any' type. - ## TS warns about invalid Emittery method calls [slow] > Snapshot 1 @@ -29,4 +11,4 @@ Generated by [AVA](https://ava.li). `test/fixtures/fails/emit-extra.ts (6,1): Expected 1-2 arguments, but got 3.␊ test/fixtures/fails/on-extra.ts (5,18): Argument of type '(data: any, more: any) => undefined' is not assignable to parameter of type '(eventData?: any) => any'.␊ test/fixtures/fails/on-extra.ts (5,19): Parameter 'data' implicitly has an 'any' type.␊ - test/fixtures/fails/on-extra.ts (5,25): Parameter 'more' implicitly has an 'any' type.` \ No newline at end of file + test/fixtures/fails/on-extra.ts (5,25): Parameter 'more' implicitly has an 'any' type.` diff --git a/test/snapshots/types.js.snap b/test/snapshots/types.js.snap index e7adc5e0395edd79b87bdc453f9003ee3cf9cdf4..25327c03cbfaea46c0b5079c0abafefc4df04bca 100644 GIT binary patch literal 311 zcmV-70m%MARzV z&%a8XB)az)#z1WMd$c8X2M-aG=d_Rw z*N%KzpsajKO7fT*b~s@V!^0#S&Ec}Kv}DR58&3gUSu4`)6Ij#?rC~Uj?EGaJy(Z0) z>?ly@U%$Z=z^x^ShdDCN0d<>zGLHIn1Sh9Z>zoRyDUU&V&>;xDm!-xvj#K~u literal 385 zcmV-{0e=2LRzVDW#J_hJ<(uNRR4{@?AFwbmFfp)$^)s?C2r>0AmZTP!=%-~?l#~{w7VD=a zX66*@r{-ps=%!Yb6ea4F6f0<$=@@ESDY#Y?q$Zc7rYIQd8Yv_erI+TW=9LudC?u7Z zD5U3?C>ZN;VKXZ~56L7`9YYJCQI1gE3i)XYC6xuK3hEjui6x0v3W<4@Itsb@MX6w> zrh=`VLTO$~YFcJqYKpo-X0bvZ(4xfR;>`5C#H5^5g_3-Qg2bZ4+|-iPA|%7W8i59= z6XzsLppydN#;Ajw0<X&jifSy#TLg?9jhdkm fHM56h)TEXH4R-;WXpb;w2*wlu=bysJGz9 Date: Tue, 2 Jan 2018 18:12:36 +0000 Subject: [PATCH 17/18] Clean up tests * Use glob to find files * Rely on CWD, which AVA already sets to the project root --- package.json | 2 +- test/snapshots/types.js.md | 2 +- test/snapshots/types.js.snap | Bin 311 -> 312 bytes test/types.js | 35 +++++++++++------------------------ 4 files changed, 13 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index d0fa856..4d48d97 100644 --- a/package.json +++ b/package.json @@ -58,8 +58,8 @@ "babel-plugin-transform-es2015-spread": "^6.22.0", "codecov": "^3.0.0", "delay": "^2.0.0", + "glob": "^7.1.2", "nyc": "^11.3.0", - "pkg-dir": "^2.0.0", "ts-node": "^4.1.0", "typescript": "^2.6.2", "xo": "*" diff --git a/test/snapshots/types.js.md b/test/snapshots/types.js.md index 8b2f9ab..7e6743a 100644 --- a/test/snapshots/types.js.md +++ b/test/snapshots/types.js.md @@ -4,7 +4,7 @@ The actual snapshot is saved in `types.js.snap`. Generated by [AVA](https://ava.li). -## TS warns about invalid Emittery method calls [slow] +## TS warns about invalid Emittery method calls > Snapshot 1 diff --git a/test/snapshots/types.js.snap b/test/snapshots/types.js.snap index 25327c03cbfaea46c0b5079c0abafefc4df04bca..f05effc9fd54501ebe4c88c78dc29c471ef8bc28 100644 GIT binary patch literal 312 zcmV-80muG9RzV z&%sK=KoADtO;H4I^$F&d7Se`PDq5%_=-s=gX)}on+1;=^(c}qy0l|YO4_-WY>m&G7 z9&{5#D-`TrW-j~v|9}u85TC2}>$iu;`ELLI?&IaTABgt7`q2}6-5%_SgWglX z&%a8XB)az)#z1WMd$c8X2M-aG=d_Rw z*N%KzpsajKO7fT*b~s@V!^0#S&Ec}Kv}DR58&3gUSu4`)6Ij#?rC~Uj?EGaJy(Z0) z>?ly@U%$Z=z^x^ShdDCN0d<>zGLHIn1Sh9Z>zoRyDUU&V&>;xDm!-xvj#K~u diff --git a/test/types.js b/test/types.js index 2f1454d..ff27ab4 100644 --- a/test/types.js +++ b/test/types.js @@ -1,12 +1,9 @@ -import {readdirSync, statSync} from 'fs'; -import {resolve as _resolve, join, extname, relative} from 'path'; -import pkgUp from 'pkg-dir'; +import path from 'path'; import test from 'ava'; +import glob from 'glob'; import * as ts from 'typescript'; -const resolve = _resolve.bind(null, __dirname); -const validExtension = new Set(['.js', '.ts']); const compilerOptions = { target: ts.ScriptTarget.ES2017, module: ts.ModuleKind.CommonJS, @@ -14,18 +11,18 @@ const compilerOptions = { noEmit: true }; -test('TS can compile valid Emittery method calls [slow]', assertAllCompile, './fixtures/compiles'); -test('TS warns about invalid Emittery method calls [slow]', assertEachFail, './fixtures/fails'); +test('TS can compile valid Emittery method calls', assertAllCompile, 'test/fixtures/compiles'); +test('TS warns about invalid Emittery method calls', assertEachFail, 'test/fixtures/fails'); function assertAllCompile(t, srcDir) { - const fileNames = listFiles(resolve(srcDir)); + const fileNames = listFiles(srcDir); const errors = compile(fileNames); t.is(errors.length, 0, errorMessage(errors)); } function assertEachFail(t, srcDir) { - const fileNames = listFiles(resolve(srcDir)).sort(); + const fileNames = listFiles(srcDir).sort(); const errors = compile(fileNames); const filesWithErrors = errors .map(err => (err.file ? err.file.fileName : null)) @@ -36,10 +33,10 @@ function assertEachFail(t, srcDir) { } function listFiles(srcRoot) { - return readdirSync(srcRoot) - .filter(hasValidExtension) - .map(name => join(srcRoot, name)) - .filter(isFile); + return glob.sync('{*.js,*.ts}', { + cwd: path.resolve(srcRoot), + absolute: true + }); } function compile(fileNames, options = compilerOptions) { @@ -50,8 +47,6 @@ function compile(fileNames, options = compilerOptions) { } function errorMessage(diagnosticList) { - const root = pkgUp.sync(); - return diagnosticList.map(diagnostic => { if (!diagnostic.file) { return `${ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n')}`; @@ -59,16 +54,8 @@ function errorMessage(diagnosticList) { const {line, character} = diagnostic.file.getLineAndCharacterOfPosition(diagnostic.start); const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, '\n'); - const fileName = relative(root, diagnostic.file.fileName); + const fileName = path.relative(process.cwd(), diagnostic.file.fileName); return `${fileName} (${line + 1},${character + 1}): ${message}`; }).join('\n'); } - -function isFile(path) { - return statSync(path).isFile(); -} - -function hasValidExtension(path) { - return validExtension.has(extname(path)); -} From 369df41c40c99d343e56cfda2498d7f4c05f61b4 Mon Sep 17 00:00:00 2001 From: Mark Wubben Date: Thu, 4 Jan 2018 13:56:53 +0000 Subject: [PATCH 18/18] Add TypeScript support to README --- Emittery.d.ts | 4 ++-- readme.md | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Emittery.d.ts b/Emittery.d.ts index 9235e18..76d7d92 100644 --- a/Emittery.d.ts +++ b/Emittery.d.ts @@ -108,8 +108,8 @@ declare namespace Emittery { * * ee.emit('open'); * ee.emit('value', 'foo\n'); - * ee.emit('value', 1); // TS emit error - * ee.emit('end'); // TS emit error + * ee.emit('value', 1); // TS compilation error + * ee.emit('end'); // TS compilation error * ``` */ class Typed extends Emittery { diff --git a/readme.md b/readme.md index 3552a86..f6330fe 100644 --- a/readme.md +++ b/readme.md @@ -106,6 +106,23 @@ Clear all event listeners on the instance. The number of listeners for the `eventName` or all events if not specified. +## TypeScript + +Definition for `emittery` and `emittery/legacy` are included. Use `import Emittery = require('emittery')` or `import Emittery = require('emittery/legacy')` to load the desired implementation. + +The default `Emittery` class does not let you type allowed event names and their associated data. However you can use `Emittery.Typed` with generics: + +```ts +import Emittery = require('emittery'); + +const ee = new Emittery.Typed<{value: string}, 'open' | 'close'>(); + +ee.emit('open'); +ee.emit('value', 'foo\n'); +ee.emit('value', 1); // TS compilation error +ee.emit('end'); // TS compilation error +``` + ## FAQ