239 lines
5.7 KiB
JavaScript
239 lines
5.7 KiB
JavaScript
|
import {dequal} from 'dequal'
|
|||
|
|
|||
|
/**
|
|||
|
* @type {Set<string>}
|
|||
|
*/
|
|||
|
const codesWarned = new Set()
|
|||
|
|
|||
|
class AssertionError extends Error {
|
|||
|
name = /** @type {const} */ ('Assertion')
|
|||
|
code = /** @type {const} */ ('ERR_ASSERTION')
|
|||
|
|
|||
|
/**
|
|||
|
* Create an assertion error.
|
|||
|
*
|
|||
|
* @param {string} message
|
|||
|
* Message explaining error.
|
|||
|
* @param {unknown} actual
|
|||
|
* Value.
|
|||
|
* @param {unknown} expected
|
|||
|
* Baseline.
|
|||
|
* @param {string} operator
|
|||
|
* Name of equality operation.
|
|||
|
* @param {boolean} generated
|
|||
|
* Whether `message` is a custom message or not
|
|||
|
* @returns
|
|||
|
* Instance.
|
|||
|
*/
|
|||
|
// eslint-disable-next-line max-params
|
|||
|
constructor(message, actual, expected, operator, generated) {
|
|||
|
super(message)
|
|||
|
|
|||
|
if (Error.captureStackTrace) {
|
|||
|
Error.captureStackTrace(this, this.constructor)
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @type {unknown}
|
|||
|
*/
|
|||
|
this.actual = actual
|
|||
|
|
|||
|
/**
|
|||
|
* @type {unknown}
|
|||
|
*/
|
|||
|
this.expected = expected
|
|||
|
|
|||
|
/**
|
|||
|
* @type {boolean}
|
|||
|
*/
|
|||
|
this.generated = generated
|
|||
|
|
|||
|
/**
|
|||
|
* @type {string}
|
|||
|
*/
|
|||
|
this.operator = operator
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
class DeprecationError extends Error {
|
|||
|
name = /** @type {const} */ ('DeprecationWarning')
|
|||
|
|
|||
|
/**
|
|||
|
* Create a deprecation message.
|
|||
|
*
|
|||
|
* @param {string} message
|
|||
|
* Message explaining deprecation.
|
|||
|
* @param {string | undefined} code
|
|||
|
* Deprecation identifier; deprecation messages will be generated only once per code.
|
|||
|
* @returns
|
|||
|
* Instance.
|
|||
|
*/
|
|||
|
constructor(message, code) {
|
|||
|
super(message)
|
|||
|
|
|||
|
/**
|
|||
|
* @type {string | undefined}
|
|||
|
*/
|
|||
|
this.code = code
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Wrap a function or class to show a deprecation message when first called.
|
|||
|
*
|
|||
|
* > 👉 **Important**: only shows a message when the `development` condition is
|
|||
|
* > used, does nothing in production.
|
|||
|
*
|
|||
|
* When the resulting wrapped `fn` is called, emits a warning once to
|
|||
|
* `console.error` (`stderr`).
|
|||
|
* If a code is given, one warning message will be emitted in total per code.
|
|||
|
*
|
|||
|
* @template {Function} T
|
|||
|
* Function or class kind.
|
|||
|
* @param {T} fn
|
|||
|
* Function or class.
|
|||
|
* @param {string} message
|
|||
|
* Message explaining deprecation.
|
|||
|
* @param {string | null | undefined} [code]
|
|||
|
* Deprecation identifier (optional); deprecation messages will be generated
|
|||
|
* only once per code.
|
|||
|
* @returns {T}
|
|||
|
* Wrapped `fn`.
|
|||
|
*/
|
|||
|
export function deprecate(fn, message, code) {
|
|||
|
let warned = false
|
|||
|
|
|||
|
// The wrapper will keep the same prototype as fn to maintain prototype chain
|
|||
|
Object.setPrototypeOf(deprecated, fn)
|
|||
|
|
|||
|
// @ts-expect-error: it’s perfect, typescript…
|
|||
|
return deprecated
|
|||
|
|
|||
|
/**
|
|||
|
* @this {unknown}
|
|||
|
* @param {...Array<unknown>} args
|
|||
|
* @returns {unknown}
|
|||
|
*/
|
|||
|
function deprecated(...args) {
|
|||
|
if (!warned) {
|
|||
|
warned = true
|
|||
|
|
|||
|
if (typeof code === 'string' && codesWarned.has(code)) {
|
|||
|
// Empty.
|
|||
|
} else {
|
|||
|
console.error(new DeprecationError(message, code || undefined))
|
|||
|
|
|||
|
if (typeof code === 'string') codesWarned.add(code)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return new.target
|
|||
|
? Reflect.construct(fn, args, new.target)
|
|||
|
: Reflect.apply(fn, this, args)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Assert deep strict equivalence.
|
|||
|
*
|
|||
|
* > 👉 **Important**: only asserts when the `development` condition is used,
|
|||
|
* > does nothing in production.
|
|||
|
*
|
|||
|
* @template {unknown} T
|
|||
|
* Expected kind.
|
|||
|
* @param {unknown} actual
|
|||
|
* Value.
|
|||
|
* @param {T} expected
|
|||
|
* Baseline.
|
|||
|
* @param {Error | string | null | undefined} [message]
|
|||
|
* Message for assertion error (default: `'Expected values to be deeply equal'`).
|
|||
|
* @returns {asserts actual is T}
|
|||
|
* Nothing; throws when `actual` is not deep strict equal to `expected`.
|
|||
|
* @throws {AssertionError}
|
|||
|
* Throws when `actual` is not deep strict equal to `expected`.
|
|||
|
*/
|
|||
|
export function equal(actual, expected, message) {
|
|||
|
assert(
|
|||
|
dequal(actual, expected),
|
|||
|
actual,
|
|||
|
expected,
|
|||
|
'equal',
|
|||
|
'Expected values to be deeply equal',
|
|||
|
message
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Assert if `value` is truthy.
|
|||
|
*
|
|||
|
* > 👉 **Important**: only asserts when the `development` condition is used,
|
|||
|
* > does nothing in production.
|
|||
|
*
|
|||
|
* @param {unknown} value
|
|||
|
* Value to assert.
|
|||
|
* @param {Error | string | null | undefined} [message]
|
|||
|
* Message for assertion error (default: `'Expected value to be truthy'`).
|
|||
|
* @returns {asserts value}
|
|||
|
* Nothing; throws when `value` is falsey.
|
|||
|
* @throws {AssertionError}
|
|||
|
* Throws when `value` is falsey.
|
|||
|
*/
|
|||
|
export function ok(value, message) {
|
|||
|
assert(
|
|||
|
Boolean(value),
|
|||
|
false,
|
|||
|
true,
|
|||
|
'ok',
|
|||
|
'Expected value to be truthy',
|
|||
|
message
|
|||
|
)
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* Assert that a code path never happens.
|
|||
|
*
|
|||
|
* > 👉 **Important**: only asserts when the `development` condition is used,
|
|||
|
* > does nothing in production.
|
|||
|
*
|
|||
|
* @param {Error | string | null | undefined} [message]
|
|||
|
* Message for assertion error (default: `'Unreachable'`).
|
|||
|
* @returns {never}
|
|||
|
* Nothing; always throws.
|
|||
|
* @throws {AssertionError}
|
|||
|
* Throws when `value` is falsey.
|
|||
|
*/
|
|||
|
export function unreachable(message) {
|
|||
|
assert(false, false, true, 'ok', 'Unreachable', message)
|
|||
|
}
|
|||
|
|
|||
|
/**
|
|||
|
* @param {boolean} bool
|
|||
|
* Whether to skip this operation.
|
|||
|
* @param {unknown} actual
|
|||
|
* Actual value.
|
|||
|
* @param {unknown} expected
|
|||
|
* Expected value.
|
|||
|
* @param {string} operator
|
|||
|
* Operator.
|
|||
|
* @param {string} defaultMessage
|
|||
|
* Default message for operation.
|
|||
|
* @param {Error | string | null | undefined} userMessage
|
|||
|
* User-provided message.
|
|||
|
* @returns {asserts bool}
|
|||
|
* Nothing; throws when falsey.
|
|||
|
*/
|
|||
|
// eslint-disable-next-line max-params
|
|||
|
function assert(bool, actual, expected, operator, defaultMessage, userMessage) {
|
|||
|
if (!bool) {
|
|||
|
throw userMessage instanceof Error
|
|||
|
? userMessage
|
|||
|
: new AssertionError(
|
|||
|
userMessage || defaultMessage,
|
|||
|
actual,
|
|||
|
expected,
|
|||
|
operator,
|
|||
|
!userMessage
|
|||
|
)
|
|||
|
}
|
|||
|
}
|