diff --git a/.gitignore b/.gitignore index 735f4af..c977c85 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store +*.d.ts *.log coverage/ node_modules/ diff --git a/index.js b/index.js index 440ad77..a762071 100644 --- a/index.js +++ b/index.js @@ -1,93 +1,131 @@ -// Create new middleware. +/** + * @typedef {(error?: Error|null|undefined, ...output: unknown[]) => void} Callback + * @typedef {(...input: unknown[]) => unknown} Middleware + * + * @typedef {(...input: unknown[]) => void} Run Call all middleware. + * @typedef {(fn: Middleware) => Pipeline} Use Add `fn` (middleware) to the list. + * @typedef {{run: Run, use: Use}} Pipeline + */ + +/** + * Create new middleware. + * + * @returns {Pipeline} + */ export function trough() { + /** @type {Middleware[]} */ var fns = [] - var middleware = {run, use} + /** @type {Pipeline} */ + var pipeline = {run, use} - return middleware + return pipeline - // Run `fns`. - // Last argument must be a completion handler. - function run(...input) { - var index = -1 - var done = input.pop() + /** @type {Run} */ + function run(...values) { + var middlewareIndex = -1 + /** @type {Callback} */ + // @ts-expect-error Assume it’s a callback. + var callback = values.pop() - if (typeof done !== 'function') { - throw new TypeError('Expected function as last argument, not ' + done) + if (typeof callback !== 'function') { + throw new TypeError('Expected function as last argument, not ' + callback) } - next(null, ...input) + next(null, ...values) - // Run the next `fn`, if any. - function next(...values) { - var fn = fns[++index] - var error = values.shift() - var pos = -1 + /** + * Run the next `fn`, or we’re done. + * + * @param {Error|null|undefined} error + * @param {unknown[]} output + */ + function next(error, ...output) { + var fn = fns[++middlewareIndex] + var index = -1 if (error) { - done(error) + callback(error) return } // Copy non-nullish input into values. - while (++pos < input.length) { - if (values[pos] === null || values[pos] === undefined) { - values[pos] = input[pos] + while (++index < values.length) { + if (output[index] === null || output[index] === undefined) { + output[index] = values[index] } } // Next or done. if (fn) { - wrap(fn, next)(...values) + wrap(fn, next)(...output) } else { - done(null, ...values) + callback(null, ...output) } } } - // Add `fn` to the list. - function use(fn) { - if (typeof fn !== 'function') { - throw new TypeError('Expected `fn` to be a function, not ' + fn) + /** @type {Use} */ + function use(middelware) { + if (typeof middelware !== 'function') { + throw new TypeError( + 'Expected `middelware` to be a function, not ' + middelware + ) } - fns.push(fn) - return middleware + fns.push(middelware) + return pipeline } } -// Wrap `fn`. -// Can be sync or async; return a promise, receive a completion handler, return -// new values and errors. -export function wrap(fn, callback) { +/** + * Wrap `middleware`. + * Can be sync or async; return a promise, receive a callback, or return new + * values and errors. + * + * @param {Middleware} middleware + * @param {Callback} callback + */ +export function wrap(middleware, callback) { + /** @type {boolean} */ var called return wrapped + /** + * Call `middleware`. + * @param {unknown[]} parameters + * @returns {void} + */ function wrapped(...parameters) { - var callback = fn.length > parameters.length + var fnExpectsCallback = middleware.length > parameters.length + /** @type {unknown} */ var result + /** @type {Error} */ + var exception - if (callback) { + if (fnExpectsCallback) { parameters.push(done) } try { - result = fn(...parameters) + result = middleware(...parameters) } catch (error) { + exception = error + // Well, this is quite the pickle. - // `fn` received a callback and called it (thus continuing the pipeline), - // but later also threw an error. - // We’re not about to restart the pipeline again, so the only thing left - // to do is to throw the thing instead. - if (callback && called) { - throw error + // `middleware` received a callback and called it synchronously, but that + // threw an error. + // The only thing left to do is to throw the thing instead. + if (fnExpectsCallback && called) { + throw exception } - return done(error) + return done(exception) } - if (!callback) { - if (result && typeof result.then === 'function') { + if (!fnExpectsCallback) { + if (result instanceof Promise) { + // type-coverage:ignore-next-line Assume it’s a `Promise` result.then(then, done) } else if (result instanceof Error) { done(result) @@ -97,16 +135,22 @@ export function wrap(fn, callback) { } } - // Call `next`, only once. - function done() { + /** + * Call `callback`, only once. + * @type {Callback} + */ + function done(error, ...output) { if (!called) { called = true - callback(...arguments) + callback(error, ...output) } } - // Call `done` with one value. - // Tracks if an error is passed, too. + /** + * Call `done` with one value. + * + * @param {unknown} [value] + */ function then(value) { done(null, value) } diff --git a/package.json b/package.json index 443f39f..1f9aee1 100644 --- a/package.json +++ b/package.json @@ -20,22 +20,30 @@ "sideEffects": false, "type": "module", "main": "index.js", + "types": "index.d.ts", "files": [ + "index.d.ts", "index.js" ], "devDependencies": { - "c8": "^7.6.0", + "@types/tape": "^4.0.0", + "c8": "^7.0.0", "prettier": "^2.0.0", "remark-cli": "^9.0.0", "remark-preset-wooorm": "^8.0.0", + "rimraf": "^3.0.0", "tape": "^5.0.0", + "type-coverage": "^2.0.0", + "typescript": "^4.0.0", "xo": "^0.38.0" }, "scripts": { + "prepack": "npm run build && npm run format", + "build": "rimraf \"*.d.ts\" && tsc && type-coverage", "format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix", "test-api": "node test.js", "test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node test.js", - "test": "npm run format && npm run test-coverage" + "test": "npm run build && npm run format && npm run test-coverage" }, "prettier": { "tabWidth": 2, @@ -48,6 +56,7 @@ "xo": { "prettier": true, "rules": { + "capitalized-comments": "off", "no-var": "off", "prefer-arrow-callback": "off" } @@ -56,5 +65,11 @@ "plugins": [ "preset-wooorm" ] + }, + "typeCoverage": { + "atLeast": 100, + "detail": true, + "strict": true, + "ignoreCatch": true } } diff --git a/readme.md b/readme.md index 97ae237..d7caaf7 100644 --- a/readme.md +++ b/readme.md @@ -76,7 +76,7 @@ There is no default export. Create a new [`Trough`][trough]. -### `wrap(middleware, callback[, …input])` +### `wrap(middleware, callback)(…input)` Call `middleware` with all input. If `middleware` accepts more arguments than given in input, an extra `done` diff --git a/test.js b/test.js index cedd9d9..e105f70 100644 --- a/test.js +++ b/test.js @@ -1,3 +1,7 @@ +/** + * @typedef {import('./index.js').Callback} Callback + */ + import test from 'tape' import {trough} from './index.js' @@ -13,15 +17,20 @@ test('use()', function (t) { t.throws( function () { + // @ts-ignore expected. p.use() }, - /Expected `fn` to be a function, not undefined/, + /Expected `middelware` to be a function, not undefined/, 'should throw without `fn`' ) p = trough() - t.equal(p.use(Function.prototype), p, 'should return self') + t.equal( + p.use(() => {}), + p, + 'should return self' + ) t.end() }) @@ -35,7 +44,7 @@ test('synchronous middleware', function (t) { .use(function () { return value }) - .run(function (error) { + .run(function (/** @type {Error} */ error) { t.equal(error, value, 'should pass returned errors to `done`') }) @@ -43,32 +52,42 @@ test('synchronous middleware', function (t) { .use(function () { throw value }) - .run(function (error) { + .run(function (/** @type {Error} */ error) { t.equal(error, value, 'should pass thrown errors to `done`') }) trough() - .use(function (value) { + .use(function (/** @type {string} */ value) { t.equal(value, 'some', 'should pass values to `fn`s') }) - .run('some', function (error, value) { - t.ifErr(error) - t.equal(value, 'some', 'should pass values to `done`') - }) + .run( + 'some', + function (/** @type {void} */ error, /** @type {string} */ value) { + t.ifErr(error) + t.equal(value, 'some', 'should pass values to `done`') + } + ) trough() - .use(function (value) { + .use(function (/** @type {string} */ value) { t.equal(value, 'some', 'should pass values to `fn`s') return value + 'thing' }) - .use(function (value) { + .use(function (/** @type {string} */ value) { t.equal(value, 'something', 'should modify values') return value + ' more' }) - .run('some', function (error, value) { - t.ifErr(error) - t.equal(value, 'something more', 'should pass modified values to `done`') - }) + .run( + 'some', + function (/** @type {void} */ error, /** @type {string} */ value) { + t.ifErr(error) + t.equal( + value, + 'something more', + 'should pass modified values to `done`' + ) + } + ) }) test('promise middleware', function (t) { @@ -82,42 +101,52 @@ test('promise middleware', function (t) { reject(value) }) }) - .run(function (error) { + .run(function (/** @type {Error} */ error) { t.equal(error, value, 'should pass rejected errors to `done`') }) trough() - .use(function (value) { + .use(function (/** @type {string} */ value) { t.equal(value, 'some', 'should pass values to `fn`s') return new Promise(function (resolve) { resolve() }) }) - .run('some', function (error, value) { - t.ifErr(error) - t.equal(value, 'some', 'should pass values to `done`') - }) + .run( + 'some', + function (/** @type {void} */ error, /** @type {string} */ value) { + t.ifErr(error) + t.equal(value, 'some', 'should pass values to `done`') + } + ) trough() - .use(function (value) { + .use(function (/** @type {string} */ value) { t.equal(value, 'some', 'should pass values to `fn`s') return new Promise(function (resolve) { resolve(value + 'thing') }) }) - .use(function (value) { + .use(function (/** @type {string} */ value) { t.equal(value, 'something', 'should modify values') return new Promise(function (resolve) { resolve(value + ' more') }) }) - .run('some', function (error, value) { - t.ifErr(error) - t.equal(value, 'something more', 'should pass modified values to `done`') - }) + .run( + 'some', + function (/** @type {void} */ error, /** @type {string} */ value) { + t.ifErr(error) + t.equal( + value, + 'something more', + 'should pass modified values to `done`' + ) + } + ) }) test('asynchronous middleware', function (t) { @@ -126,34 +155,34 @@ test('asynchronous middleware', function (t) { t.plan(11) trough() - .use(function (next) { + .use(function (/** @type {Callback} */ next) { next(value) }) - .run(function (error) { + .run(function (/** @type {Error} */ error) { t.equal(error, value, 'should pass given errors to `done`') }) trough() - .use(function (next) { + .use(function (/** @type {Callback} */ next) { setImmediate(function () { next(value) }) }) - .run(function (error) { + .run(function (/** @type {Error} */ error) { t.equal(error, value, 'should pass async given errors to `done`') }) trough() - .use(function (next) { + .use(function (/** @type {Callback} */ next) { next(value) next(new Error('Other')) }) - .run(function (error) { + .run(function (/** @type {Error} */ error) { t.equal(error, value, 'should ignore multiple sync `next` calls') }) trough() - .use(function (next) { + .use(function (/** @type {Callback} */ next) { setImmediate(function () { next(value) setImmediate(function () { @@ -161,39 +190,49 @@ test('asynchronous middleware', function (t) { }) }) }) - .run(function (error) { + .run(function (/** @type {Error} */ error) { t.equal(error, value, 'should ignore multiple async `next` calls') }) trough() - .use(function (value, next) { + .use(function (/** @type {string} */ value, /** @type {Callback} */ next) { t.equal(value, 'some', 'should pass values to `fn`s') setImmediate(next) }) - .run('some', function (error, value) { - t.ifErr(error) - t.equal(value, 'some', 'should pass values to `done`') - }) + .run( + 'some', + function (/** @type {void} */ error, /** @type {string} */ value) { + t.ifErr(error) + t.equal(value, 'some', 'should pass values to `done`') + } + ) trough() - .use(function (value, next) { + .use(function (/** @type {string} */ value, /** @type {Callback} */ next) { t.equal(value, 'some', 'should pass values to `fn`s') setImmediate(function () { next(null, value + 'thing') }) }) - .use(function (value, next) { + .use(function (/** @type {string} */ value, /** @type {Callback} */ next) { t.equal(value, 'something', 'should modify values') setImmediate(function () { next(null, value + ' more') }) }) - .run('some', function (error, value) { - t.ifErr(error) - t.equal(value, 'something more', 'should pass modified values to `done`') - }) + .run( + 'some', + function (/** @type {void} */ error, /** @type {string} */ value) { + t.ifErr(error) + t.equal( + value, + 'something more', + 'should pass modified values to `done`' + ) + } + ) }) test('run()', function (t) { @@ -201,6 +240,7 @@ test('run()', function (t) { t.throws( function () { + // @ts-ignore expected. trough().run() }, /^TypeError: Expected function as last argument, not undefined$/, @@ -208,29 +248,32 @@ test('run()', function (t) { ) trough() - .use(function (value) { + .use(function (/** @type {string} */ value) { t.equal(value, 'some', 'input') return value + 'thing' }) - .use(function (value) { + .use(function (/** @type {string} */ value) { t.equal(value, 'something', 'sync') return new Promise(function (resolve) { resolve(value + ' more') }) }) - .use(function (value, next) { + .use(function (/** @type {string} */ value, /** @type {Callback} */ next) { t.equal(value, 'something more', 'promise') setImmediate(function () { next(null, value + '.') }) }) - .run('some', function (error, value) { - t.ifErr(error) - t.equal(value, 'something more.', 'async') - }) + .run( + 'some', + function (/** @type {void} */ error, /** @type {string} */ value) { + t.ifErr(error) + t.equal(value, 'something more.', 'async') + } + ) t.test('should throw errors thrown from `done` (#1)', function (st) { st.plan(1) @@ -250,7 +293,7 @@ test('run()', function (t) { }) trough() - .use(function (next) { + .use(function (/** @type {Callback} */ next) { setImmediate(next) }) .run(function () { @@ -266,12 +309,12 @@ test('run()', function (t) { }) trough() - .use(function (next) { + .use(function (/** @type {Callback} */ next) { setImmediate(function () { next(new Error('bravo')) }) }) - .run(function (error) { + .run(function (/** @type {Error} */ error) { throw error }) }) @@ -287,7 +330,7 @@ test('run()', function (t) { .use(function () { throw new Error('bravo') }) - .run(function (error) { + .run(function (/** @type {Error} */ error) { throw error }) }) @@ -300,7 +343,7 @@ test('run()', function (t) { }) trough() - .use(function (next) { + .use(function (/** @type {Callback} */ next) { setImmediate(next) }) .run(function () { @@ -318,12 +361,12 @@ test('run()', function (t) { }) trough() - .use(function (next) { + .use(function (/** @type {Callback} */ next) { setImmediate(function () { next(new Error('charlie')) }) }) - .run(function (error) { + .run(function (/** @type {Error} */ error) { setImmediate(function () { throw error }) @@ -340,10 +383,10 @@ test('run()', function (t) { }) trough() - .use(function (next) { + .use(function (/** @type {Callback} */ next) { next(value) }) - .run(function (error) { + .run(function (/** @type {Error} */ error) { throw error }) }) diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8ac10fe --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "files": ["index.js"], + "include": ["*.js"], + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020"], + "module": "ES2020", + "moduleResolution": "node", + "allowJs": true, + "checkJs": true, + "declaration": true, + "emitDeclarationOnly": true, + "allowSyntheticDefaultImports": true, + "skipLibCheck": true + } +}