361 lines
9.9 KiB
Markdown
361 lines
9.9 KiB
Markdown
# devlop
|
||
|
||
[![Build][build-badge]][build]
|
||
[![Coverage][coverage-badge]][coverage]
|
||
[![Downloads][downloads-badge]][downloads]
|
||
[![Size][size-badge]][size]
|
||
|
||
Some tools to make developing easier while not including code in production.
|
||
|
||
## Contents
|
||
|
||
* [What is this?](#what-is-this)
|
||
* [When should I use this?](#when-should-i-use-this)
|
||
* [Install](#install)
|
||
* [Use](#use)
|
||
* [API](#api)
|
||
* [`deprecate(fn, message[, code])`](#deprecatefn-message-code)
|
||
* [`equal(actual, expected[, message])`](#equalactual-expected-message)
|
||
* [`ok(value[, message])`](#okvalue-message)
|
||
* [`unreachable(message?)`](#unreachablemessage)
|
||
* [Types](#types)
|
||
* [Compatibility](#compatibility)
|
||
* [Security](#security)
|
||
* [Related](#related)
|
||
* [Contribute](#contribute)
|
||
* [License](#license)
|
||
|
||
## What is this?
|
||
|
||
This package lets you do things in development that are free in production.
|
||
It contains useful `assert` functions and a `deprecate` function that are
|
||
useful when developing JavaScript packages while being small in production.
|
||
|
||
If you know Rust, you might know how nice having a
|
||
[`debug_assert!`][rust-debug-assert] is.
|
||
This is that, and a bit more.
|
||
For more on why they’re nice, see
|
||
[“Rust’s Two Kinds of ‘Assert’ Make for Better Code”][rust-two-kinds]
|
||
|
||
## When should I use this?
|
||
|
||
Many JavaScript programs do not use assertions at all (perhaps because they’re
|
||
typed and so assume type safety) or include lots of code to throw errors when
|
||
users do weird things (weighing down production code).
|
||
This package hopes to improve the sitation by making assertions free and
|
||
deprecations cheap.
|
||
|
||
## Install
|
||
|
||
This package is [ESM only][esm].
|
||
In Node.js (version 16+), install with [npm][]:
|
||
|
||
```sh
|
||
npm install devlop
|
||
```
|
||
|
||
In Deno with [`esm.sh`][esmsh]:
|
||
|
||
```js
|
||
import {deprecate, equal, ok, unreachable} from 'https://esm.sh/devlop@1'
|
||
// For development code:
|
||
// import {deprecate, equal, ok} from 'https://esm.sh/devlop@1?conditions=development'
|
||
```
|
||
|
||
In browsers with [`esm.sh`][esmsh]:
|
||
|
||
```html
|
||
<script type="module">
|
||
import {deprecate, equal, ok, unreachable} from 'https://esm.sh/devlop@1?bundle'
|
||
// For development code:
|
||
// import {deprecate, equal, ok} from 'https://esm.sh/devlop@1?bundle&conditions=development'
|
||
</script>
|
||
```
|
||
|
||
## Use
|
||
|
||
Say we have a small ponyfill for the ES5 `String#includes` function.
|
||
It’s deprecated, because folks can use `String#includes` nowadays.
|
||
It’s nicely typed so users should be able to figure out what to pass but we
|
||
include assertions to show nicer errors when they get it wrong.
|
||
|
||
`example/string-includes.js`:
|
||
|
||
```js
|
||
import {deprecate, ok} from 'devlop'
|
||
|
||
export const stringIncludes = deprecate(
|
||
includes,
|
||
'Since ES5, please use `String#includes` itself.'
|
||
)
|
||
|
||
/**
|
||
* @deprecated
|
||
* Since ES5, please use `String#includes` itself.
|
||
* @param {string} value
|
||
* Value to search in.
|
||
* @param {string} search
|
||
* Value to search for.
|
||
* @param {number | undefined} [position=0]
|
||
* Position to search from (default: `0`).
|
||
* @returns {boolean}
|
||
* Whether the searched for value exists in the searched value after position.
|
||
*/
|
||
function includes(value, search, position) {
|
||
ok(typeof value === 'string', 'expected string for `value`')
|
||
ok(typeof search === 'string', 'expected string for `search`')
|
||
ok(position === undefined || typeof position === 'number', 'expected number')
|
||
ok(
|
||
position === undefined ||
|
||
(typeof position === 'number' &&
|
||
!(/* #__PURE__ */ Number.isNaN(position))),
|
||
'expected number'
|
||
)
|
||
// eslint-disable-next-line unicorn/prefer-includes
|
||
return value.indexOf(search, position || 0) !== -1
|
||
}
|
||
```
|
||
|
||
`example/index.js`:
|
||
|
||
```js
|
||
import {stringIncludes} from './example-includes.js'
|
||
|
||
console.log(stringIncludes('blue whale', 'dolphin')) //=> false
|
||
console.log(stringIncludes('blue whale', 'whale')) //=> true
|
||
```
|
||
|
||
Say we’d bundle that in development with [`esbuild`][esbuild] and check the
|
||
gzip size ([`gzip-size-cli`][gzip-size-cli]), we’d get 1.02 kB of code:
|
||
|
||
```sh
|
||
$ esbuild example/index.js --bundle --conditions=development --format=esm --minify --target=es2022 | gzip-size
|
||
1.02 kB
|
||
```
|
||
|
||
But because `devlop` is light in production we’d get:
|
||
|
||
```sh
|
||
$ esbuild example/index.js --bundle --format=esm --minify --target=es2022 | gzip-size
|
||
169 B
|
||
```
|
||
|
||
The bundle looks as follows:
|
||
|
||
```js
|
||
function u(n){return n}var r=u(c,"Since ES5, please use `String#includes` itself.");function c(n,t,e){return n.indexOf(t,e||0)!==-1}console.log(r("blue whale","dolphin"));console.log(r("blue whale","whale"));
|
||
```
|
||
|
||
It depends a bit on which bundler and minifier you use how small the code is:
|
||
esbuild keeps the unused message parameter to the `deprecate` function around
|
||
and does not know `Number.isNaN` can be dropped without a `/* #__PURE__ */`
|
||
annotation.
|
||
|
||
[`rollup`][rollup] with [`@rollup/plugin-node-resolve`][node-resolve]
|
||
and [`@rollup/plugin-terser`][terser] performs even better:
|
||
|
||
```sh
|
||
$ rollup example/index.js -p node-resolve -p terser | gzip-size
|
||
118 B
|
||
```
|
||
|
||
The bundle looks as follows:
|
||
|
||
```js
|
||
const l=function(l,e,o){return-1!==l.indexOf(e,o||0)};console.log(l("blue whale","dolphin")),console.log(l("blue whale","whale"));
|
||
```
|
||
|
||
Rollup doesn’t need the `/* #__PURE__ */` comment either!
|
||
|
||
## API
|
||
|
||
This package exports the identifiers [`deprecate`][api-deprecate],
|
||
[`equal`][api-equal], [`ok`][api-ok], and [`unreachable`][api-unreachable].
|
||
There is no default export.
|
||
|
||
The export map supports the [`development` condition][node-condition].
|
||
Run `node --conditions development module.js` to get dev code.
|
||
Without this condition, no-ops are loaded.
|
||
|
||
### `deprecate(fn, message[, 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.
|
||
|
||
###### Parameters
|
||
|
||
* `fn` (`Function`)
|
||
— function or class
|
||
* `message` (`string`)
|
||
— message explaining deprecation
|
||
* `code` (`string`, optional)
|
||
— deprecation identifier (optional); deprecation messages will be generated
|
||
only once per code
|
||
|
||
###### Returns
|
||
|
||
Wrapped `fn`.
|
||
|
||
### `equal(actual, expected[, message])`
|
||
|
||
Assert deep strict equivalence.
|
||
|
||
> 👉 **Important**: only asserts when the `development` condition is used, does
|
||
> nothing in production.
|
||
|
||
###### Parameters
|
||
|
||
* `actual` (`unknown`)
|
||
— value
|
||
* `expected` (`unknown`)
|
||
— baseline
|
||
* `message` (`Error` or `string`, default: `'Expected values to be deeply
|
||
equal'`)
|
||
— message for assertion error
|
||
|
||
###### Returns
|
||
|
||
Nothing (`undefined`).
|
||
|
||
###### Throws
|
||
|
||
Throws (`AssertionError`) when `actual` is not deep strict equal to `expected`.
|
||
|
||
### `ok(value[, message])`
|
||
|
||
Assert if `value` is truthy.
|
||
|
||
> 👉 **Important**: only asserts when the `development` condition is used, does
|
||
> nothing in production.
|
||
|
||
###### Parameters
|
||
|
||
* `actual` (`unknown`)
|
||
— value to assert
|
||
* `message` (`Error` or `string`, default: `'Expected value to be truthy'`)
|
||
— message for assertion error
|
||
|
||
###### Returns
|
||
|
||
Nothing (`undefined`).
|
||
|
||
###### Throws
|
||
|
||
Throws (`AssertionError`) when `value` is falsey.
|
||
|
||
### `unreachable(message?)`
|
||
|
||
Assert that a code path never happens.
|
||
|
||
> 👉 **Important**: only asserts when the `development` condition is used,
|
||
> does nothing in production.
|
||
|
||
###### Parameters
|
||
|
||
* `message` (`Error` or `string`, default: `'Unreachable'`)
|
||
— message for assertion error
|
||
|
||
###### Returns
|
||
|
||
Never (`never`).
|
||
|
||
###### Throws
|
||
|
||
Throws (`AssertionError`), always.
|
||
|
||
## Types
|
||
|
||
This package is fully typed with [TypeScript][].
|
||
It exports no additional types.
|
||
|
||
## Compatibility
|
||
|
||
This project is compatible with maintained versions of Node.js.
|
||
|
||
When we cut a new major release, we drop support for unmaintained versions of
|
||
Node.
|
||
This means we try to keep the current release line, `devlop@^1`,
|
||
compatible with Node.js 16.
|
||
|
||
## Security
|
||
|
||
This package is safe.
|
||
|
||
## Related
|
||
|
||
* [`babel-plugin-unassert`](https://github.com/unassert-js/babel-plugin-unassert)
|
||
— encourage reliable programming with assertions while compiling them away
|
||
in production (can remove arbitrary `assert` modules, works regardless of
|
||
conditions, so has to be configured by the end user)
|
||
|
||
## Contribute
|
||
|
||
Yes please!
|
||
See [How to Contribute to Open Source][contribute].
|
||
|
||
## License
|
||
|
||
[MIT][license] © [Titus Wormer][author]
|
||
|
||
<!-- Definition -->
|
||
|
||
[build-badge]: https://github.com/wooorm/devlop/workflows/main/badge.svg
|
||
|
||
[build]: https://github.com/wooorm/devlop/actions
|
||
|
||
[coverage-badge]: https://img.shields.io/codecov/c/github/wooorm/devlop.svg
|
||
|
||
[coverage]: https://codecov.io/github/wooorm/devlop
|
||
|
||
[downloads-badge]: https://img.shields.io/npm/dm/devlop.svg
|
||
|
||
[downloads]: https://www.npmjs.com/package/devlop
|
||
|
||
[size-badge]: https://img.shields.io/badge/dynamic/json?label=minzipped%20size&query=$.size.compressedSize&url=https://deno.bundlejs.com/?q=devlop
|
||
|
||
[size]: https://bundlejs.com/?q=devlop
|
||
|
||
[npm]: https://docs.npmjs.com/cli/install
|
||
|
||
[esmsh]: https://esm.sh
|
||
|
||
[license]: license
|
||
|
||
[author]: https://wooorm.com
|
||
|
||
[esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c
|
||
|
||
[typescript]: https://www.typescriptlang.org
|
||
|
||
[contribute]: https://opensource.guide/how-to-contribute/
|
||
|
||
[node-condition]: https://nodejs.org/api/packages.html#packages_resolving_user_conditions
|
||
|
||
[rust-debug-assert]: https://doc.rust-lang.org/std/macro.debug_assert.html
|
||
|
||
[rust-two-kinds]: https://tratt.net/laurie/blog/2023/rusts_two_kinds_of_assert_make_for_better_code.html
|
||
|
||
[esbuild]: https://esbuild.github.io
|
||
|
||
[gzip-size-cli]: https://github.com/sindresorhus/gzip-size-cli/tree/main
|
||
|
||
[rollup]: https://rollupjs.org
|
||
|
||
[node-resolve]: https://github.com/rollup/plugins/tree/master/packages/node-resolve
|
||
|
||
[terser]: https://github.com/rollup/plugins/tree/master/packages/terser#readme
|
||
|
||
[api-deprecate]: #deprecatefn-message-code
|
||
|
||
[api-equal]: #equalactual-expected-message
|
||
|
||
[api-ok]: #okvalue-message
|
||
|
||
[api-unreachable]: #unreachablemessage
|