tape-six-invariant 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +28 -0
- package/README.md +116 -0
- package/index.d.ts +40 -0
- package/index.js +43 -0
- package/llms-full.txt +174 -0
- package/llms.txt +67 -0
- package/package.json +79 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Eugene Lazutkin
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# tape-six-invariant [![NPM version][npm-img]][npm-url]
|
|
2
|
+
|
|
3
|
+
[npm-img]: https://img.shields.io/npm/v/tape-six-invariant.svg
|
|
4
|
+
[npm-url]: https://npmjs.org/package/tape-six-invariant
|
|
5
|
+
|
|
6
|
+
`tape-six-invariant` lets you embed `assert`-style **invariant checks** in production or library code that **materialize** into real [`tape-six`](https://github.com/uhop/tape-six) assertions when a tape-six run exercises that code — and stay inert (or do whatever you configure) otherwise.
|
|
7
|
+
|
|
8
|
+
**Zero runtime dependencies.** Works in [Node](https://nodejs.org/), [Deno](https://deno.land/), [Bun](https://bun.sh/), and browsers. ES modules, TypeScript bindings included. The library never imports tape-six — the two coordinate through a single global slot.
|
|
9
|
+
|
|
10
|
+
```js
|
|
11
|
+
import check from 'tape-six-invariant';
|
|
12
|
+
|
|
13
|
+
export function transfer(from, to, amount) {
|
|
14
|
+
check(amount > 0, 'amount must be positive');
|
|
15
|
+
check(from.currency === to.currency, 'currencies must match');
|
|
16
|
+
// …
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
- **Under a tape-six run** (the code is exercised by a test): each `check()` becomes a counted assertion on the _current_ test — in the plan, in TAP output, with its source location pointing at the `check()` call site.
|
|
21
|
+
- **In production** (no tape-six): a configurable behavior. Default **inert** (does nothing). Overridable to throw, warn, defer to your own `node:assert`, or anything you like.
|
|
22
|
+
|
|
23
|
+
The same call site means the same thing — an invariant — and is paid for only when something is listening.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install --save tape-six-invariant
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`tape-six` is needed only to _run_ the invariants as assertions (a dev/test concern), so it is not a dependency of this package.
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
### Materialize under test
|
|
36
|
+
|
|
37
|
+
Any code reached — directly or transitively — from a tape-six test body routes its `check()` calls to the current test:
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
import test from 'tape-six';
|
|
41
|
+
import {transfer} from '../src/bank.js';
|
|
42
|
+
|
|
43
|
+
test('transfer enforces its invariants', t => {
|
|
44
|
+
transfer({currency: 'USD'}, {currency: 'USD'}, 10); // the two checks pass as assertions
|
|
45
|
+
t.pass('done');
|
|
46
|
+
});
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Configure production behavior
|
|
50
|
+
|
|
51
|
+
By default invariants are inert in production — they cost a single global-property read. Opt into enforcement at startup:
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
import {setAbsentBehavior, throwOnFail} from 'tape-six-invariant';
|
|
55
|
+
|
|
56
|
+
setAbsentBehavior(throwOnFail); // a failing check now throws InvariantError
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
With no behavior set — the default — a failing check is a no-op. Canned behaviors (exported functions, not magic strings):
|
|
60
|
+
|
|
61
|
+
| Behavior | Effect on a failing check |
|
|
62
|
+
| ------------- | ------------------------- |
|
|
63
|
+
| `throwOnFail` | throw `InvariantError` |
|
|
64
|
+
| `warnOnFail` | `console.warn` |
|
|
65
|
+
|
|
66
|
+
`setAbsentBehavior(null)` (or any non-function) clears a previously set behavior. Or pass your own `(ok, message) => void` — for instance, to defer to `node:assert`, bring your own import so it stays your dependency, not the package's:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
import assert from 'node:assert';
|
|
70
|
+
|
|
71
|
+
setAbsentBehavior((ok, message) => assert.ok(ok, message));
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Skip expensive pre-check work
|
|
75
|
+
|
|
76
|
+
`hasHost` is an import-time snapshot — use it to avoid computing an argument that is only worth checking under a run:
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
import {check, hasHost} from 'tape-six-invariant';
|
|
80
|
+
|
|
81
|
+
if (hasHost) check(expensiveToCompute(), 'invariant holds');
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Correctness never depends on the snapshot — `check()` always reads the live slot.
|
|
85
|
+
|
|
86
|
+
### Lazy messages
|
|
87
|
+
|
|
88
|
+
Pass a `() => string` thunk to build an expensive message only when it is needed:
|
|
89
|
+
|
|
90
|
+
```js
|
|
91
|
+
check(list.every(valid), () => `invalid items: ${list.filter(x => !valid(x)).join(', ')}`);
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## API
|
|
95
|
+
|
|
96
|
+
See the [wiki](https://github.com/uhop/tape-six-invariant/wiki) for full docs. `check` is the default export (and a named export); everything else is named-only.
|
|
97
|
+
|
|
98
|
+
- `check(cond, message?)` — record an invariant. `message` may be a string or a `() => string` thunk. TypeScript signature is `asserts cond`.
|
|
99
|
+
- `hasHost` — import-time boolean: was a tape-six host present at load?
|
|
100
|
+
- `setAbsentBehavior(fn)` — set the no-host failure behavior; `null` clears it (default: none, a no-op).
|
|
101
|
+
- `throwOnFail`, `warnOnFail` — canned absent behaviors.
|
|
102
|
+
- `InvariantError` — thrown by `throwOnFail`.
|
|
103
|
+
|
|
104
|
+
## Related packages
|
|
105
|
+
|
|
106
|
+
- [tape-six](https://www.npmjs.com/package/tape-six) — the test library these invariants materialize into.
|
|
107
|
+
- [tape-six-proc](https://www.npmjs.com/package/tape-six-proc) — process-isolated test execution.
|
|
108
|
+
- [tape-six-puppeteer](https://www.npmjs.com/package/tape-six-puppeteer) / [tape-six-playwright](https://www.npmjs.com/package/tape-six-playwright) — browser automation.
|
|
109
|
+
|
|
110
|
+
## Release notes
|
|
111
|
+
|
|
112
|
+
- **1.0.0** — Initial release: `check`, `hasHost`, `setAbsentBehavior`, canned behaviors (`throwOnFail`/`warnOnFail`), `InvariantError`.
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
[BSD-3-Clause](./LICENSE) © Eugene Lazutkin
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Absent-path behavior — runs when a failing {@link check} finds no host.
|
|
3
|
+
* @see https://github.com/uhop/tape-six-invariant/wiki/API#canned-behaviors
|
|
4
|
+
*/
|
|
5
|
+
export type AbsentBehavior = (ok: unknown, message?: string) => void;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Records an invariant: a counted tape-six assertion when a run is hosting it,
|
|
9
|
+
* the configured absent behavior otherwise.
|
|
10
|
+
* @see https://github.com/uhop/tape-six-invariant/wiki/API#check
|
|
11
|
+
*/
|
|
12
|
+
export declare function check(cond: unknown, message?: string | (() => string)): asserts cond;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Import-time snapshot of whether a tape-six host was present at load. Gate for
|
|
16
|
+
* skipping expensive pre-check work; correctness never depends on it.
|
|
17
|
+
* @see https://github.com/uhop/tape-six-invariant/wiki/API#hashost
|
|
18
|
+
*/
|
|
19
|
+
export declare const hasHost: boolean;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Sets the absent-path behavior. Pass `null` (or any non-function) to clear it;
|
|
23
|
+
* with none set — the default — a failing {@link check} with no host is a no-op.
|
|
24
|
+
* @see https://github.com/uhop/tape-six-invariant/wiki/API#setabsentbehavior
|
|
25
|
+
*/
|
|
26
|
+
export declare function setAbsentBehavior(fn: AbsentBehavior | null): void;
|
|
27
|
+
|
|
28
|
+
/** Absent behavior: throw {@link InvariantError} on failure. */
|
|
29
|
+
export declare const throwOnFail: AbsentBehavior;
|
|
30
|
+
|
|
31
|
+
/** Absent behavior: `console.warn` on failure. */
|
|
32
|
+
export declare const warnOnFail: AbsentBehavior;
|
|
33
|
+
|
|
34
|
+
/** Error thrown by {@link throwOnFail}. */
|
|
35
|
+
export declare class InvariantError extends Error {
|
|
36
|
+
constructor(message?: string);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** {@link check} is also the default export. */
|
|
40
|
+
export default check;
|
package/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// @ts-self-types="./index.d.ts"
|
|
2
|
+
|
|
3
|
+
const KEY = Symbol.for('tape6.invariant.host.v1');
|
|
4
|
+
|
|
5
|
+
const resolveMessage = message => (typeof message === 'function' ? message() : message);
|
|
6
|
+
|
|
7
|
+
export const hasHost = !!globalThis[KEY];
|
|
8
|
+
|
|
9
|
+
let absentBehavior = null;
|
|
10
|
+
|
|
11
|
+
export const setAbsentBehavior = fn => {
|
|
12
|
+
absentBehavior = typeof fn === 'function' ? fn : null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export class InvariantError extends Error {
|
|
16
|
+
constructor(message) {
|
|
17
|
+
super(message || 'Invariant failed');
|
|
18
|
+
this.name = 'InvariantError';
|
|
19
|
+
if (typeof Error.captureStackTrace == 'function') {
|
|
20
|
+
Error.captureStackTrace(this, InvariantError);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const throwOnFail = (ok, message) => {
|
|
26
|
+
if (!ok) throw new InvariantError(message);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const warnOnFail = (ok, message) => {
|
|
30
|
+
if (!ok) console.warn('Invariant failed' + (message ? ': ' + message : ''));
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const check = (cond, message) => {
|
|
34
|
+
const host = globalThis[KEY];
|
|
35
|
+
if (host) {
|
|
36
|
+
host.report({ok: !!cond, message: resolveMessage(message), marker: new Error()});
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (!absentBehavior) return;
|
|
40
|
+
if (!cond) absentBehavior(cond, resolveMessage(message));
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default check;
|
package/llms-full.txt
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# tape-six-invariant
|
|
2
|
+
|
|
3
|
+
> A zero-dependency library of `assert`-style invariant checks that **materialize** into real [tape-six](https://github.com/uhop/tape-six) assertions when a tape-six run exercises the code, and are inert (or a configurable behavior) in production. The library never imports tape-six; the two coordinate purely through a versioned global slot, `Symbol.for('tape6.invariant.host.v1')`.
|
|
4
|
+
|
|
5
|
+
- Embed invariants in production / library code; they become counted test assertions only when something is listening.
|
|
6
|
+
- Zero runtime dependencies; works in Node, Deno, Bun, and browsers.
|
|
7
|
+
- ES modules, no build step, hand-written TypeScript definitions (`asserts cond` narrowing on `check`).
|
|
8
|
+
- Configurable production behavior via a single generic setter (not an env-flag menu).
|
|
9
|
+
- Designed to be build-time strippable (unassert-style) — `check` stays statically matchable.
|
|
10
|
+
|
|
11
|
+
## The idea
|
|
12
|
+
|
|
13
|
+
An invariant check embedded in production or library code:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import check from 'tape-six-invariant';
|
|
17
|
+
|
|
18
|
+
export function transfer(from, to, amount) {
|
|
19
|
+
check(amount > 0, 'amount must be positive');
|
|
20
|
+
check(from.currency === to.currency, 'currencies must match');
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- **Under a tape-six run** (the code is exercised by a test): each `check()` becomes a counted assertion on the _current_ test — in the plan, in TAP output, with source location pointing at the `check()` call site.
|
|
25
|
+
- **In production** (no tape-six): a configurable behavior. Default is none — a failing check is a no-op. Overridable to throw, warn, defer to your own `node:assert`, or anything else.
|
|
26
|
+
|
|
27
|
+
The same call site means the same thing — an invariant — and is paid for only when something is listening. This routing-to-a-live-harness is the one new element vs. prior art (tiny-invariant, zertosh/invariant, unassert), which is always-on-or-stripped.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install --save tape-six-invariant
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
`tape-six` is needed only to run the invariants as assertions, so it is not a dependency of this package. To run the test suite of this repo against the unreleased host hooks, `npm link` a local tape-six checkout (see the repository AGENTS.md).
|
|
36
|
+
|
|
37
|
+
## API
|
|
38
|
+
|
|
39
|
+
### check(cond, message?)
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
function check(cond: unknown, message?: string | (() => string)): asserts cond;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Records an invariant. It is the package's **default export** and also a named export — `import check from 'tape-six-invariant'` for the common single-import case, or `import {check, hasHost} from 'tape-six-invariant'` when pulling several symbols. Every other symbol is named-only.
|
|
46
|
+
|
|
47
|
+
- `cond` — pass/fail verdict; a falsy value fails the invariant. Normalized with `!!` when reported.
|
|
48
|
+
- `message` — failure message. May be a `() => string` thunk so an expensive message is built only when resolved.
|
|
49
|
+
|
|
50
|
+
Behavior:
|
|
51
|
+
|
|
52
|
+
- **Host present** — reports `{ok: !!cond, message, marker: new Error()}` to the host, which routes it to the current tester. The `marker` Error keeps the reported source location at the call site. The message thunk (if any) is resolved before reporting.
|
|
53
|
+
- **No host** — on a falsy `cond`, runs the configured absent behavior with `(cond, resolvedMessage)`. A truthy `cond` is an early return: the only cost when off is one global-property read.
|
|
54
|
+
|
|
55
|
+
The TypeScript `asserts cond` signature narrows the condition for callers.
|
|
56
|
+
|
|
57
|
+
```js
|
|
58
|
+
import check from 'tape-six-invariant';
|
|
59
|
+
|
|
60
|
+
function area(w, h) {
|
|
61
|
+
check(w > 0 && h > 0, 'dimensions must be positive');
|
|
62
|
+
return w * h;
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### hasHost
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
const hasHost: boolean;
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Import-time snapshot of whether a tape-six host was installed when this module loaded. Reliable because tape-six sets the slot at module load and test files import tape-six first; in pure production the slot is absent and `hasHost` is `false`.
|
|
73
|
+
|
|
74
|
+
Use it to gate expensive pre-check computation:
|
|
75
|
+
|
|
76
|
+
```js
|
|
77
|
+
import {check, hasHost} from 'tape-six-invariant';
|
|
78
|
+
|
|
79
|
+
if (hasHost) check(expensiveToCompute(), 'invariant holds');
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Correctness never depends on the snapshot — `check()` reads the live slot per call.
|
|
83
|
+
|
|
84
|
+
### setAbsentBehavior(fn)
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
type AbsentBehavior = (ok: unknown, message?: string) => void;
|
|
88
|
+
function setAbsentBehavior(fn: AbsentBehavior): void;
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Replaces the absent-path behavior — what a failing `check` does when no host is present. Pass a canned behavior or your own. The default is none (a failing check is a no-op); passing `null` or any non-function clears a set behavior. Module-scoped: each copy of the library configures itself independently (only the host slot is global).
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
import {setAbsentBehavior, throwOnFail} from 'tape-six-invariant';
|
|
95
|
+
|
|
96
|
+
setAbsentBehavior(throwOnFail);
|
|
97
|
+
|
|
98
|
+
// custom:
|
|
99
|
+
setAbsentBehavior((ok, message) => {
|
|
100
|
+
if (!ok) metrics.increment('invariant.failed', {message});
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Canned behaviors
|
|
105
|
+
|
|
106
|
+
Exported functions passed to `setAbsentBehavior` (not magic strings). With none set — the default — a failing check is a no-op:
|
|
107
|
+
|
|
108
|
+
| Behavior | Effect on a failing check | Use |
|
|
109
|
+
| ------------- | ------------------------------- | -------------------------- |
|
|
110
|
+
| `throwOnFail` | throw `InvariantError(message)` | fail-fast enforcement |
|
|
111
|
+
| `warnOnFail` | `console.warn` | non-fatal telemetry signal |
|
|
112
|
+
|
|
113
|
+
To defer to `node:assert` (or any other assertion library), bring your own import and pass it to `setAbsentBehavior`. There is no canned `nodeAssert` on purpose: a synchronous `check` should not dynamically import a module, and keeping the import on the caller's side leaves this package zero-dependency.
|
|
114
|
+
|
|
115
|
+
```js
|
|
116
|
+
import assert from 'node:assert';
|
|
117
|
+
|
|
118
|
+
setAbsentBehavior((ok, message) => assert.ok(ok, message));
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### InvariantError
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
class InvariantError extends Error {
|
|
125
|
+
constructor(message?: string);
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Thrown by `throwOnFail`. `name` is `'InvariantError'`; the default message is `'Invariant failed'`.
|
|
130
|
+
|
|
131
|
+
## Coordination protocol
|
|
132
|
+
|
|
133
|
+
The library and tape-six coordinate through a **versioned, Symbol-keyed slot on `globalThis`**:
|
|
134
|
+
|
|
135
|
+
```js
|
|
136
|
+
const KEY = Symbol.for('tape6.invariant.host.v1');
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
**Why a global rather than a module variable:** in production the dependency graph can contain several copies/versions of the library (npm does not always dedupe across version ranges). The coordination point must be shared across all copies — a `globalThis` slot is; a module-scoped variable is not. Same precedent as the React DevTools global hook.
|
|
140
|
+
|
|
141
|
+
**tape-six sets the slot at module load** (not in `init()`), so `hasHost` is reliable — a test file imports tape-six first, so the slot exists before any code-under-test runs:
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
globalThis[Symbol.for('tape6.invariant.host.v1')] ||= {
|
|
145
|
+
version: 1,
|
|
146
|
+
report(assertion) {
|
|
147
|
+
const tester = getTester(); // live current tester; null between/outside tests
|
|
148
|
+
if (tester) tester.reportAssertion(assertion);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`||=` so the first-loaded tape-six copy wins (redundant copies are functionally identical).
|
|
154
|
+
|
|
155
|
+
**Cross-realm:** `Symbol.for` is shared within a realm, but each realm (worker thread, iframe, subprocess) has its own `globalThis`. tape-six already runs per worker/process and isolates per realm, so each realm's load sets its own slot.
|
|
156
|
+
|
|
157
|
+
The tape-six side provides three hooks that back this protocol: `getTester()` (innermost running tester or `null`), `Tester.reportAssertion({ok, message?, marker?, operator?, expected?, actual?})` (a marker-preserving report primitive), and the host slot itself.
|
|
158
|
+
|
|
159
|
+
## Zero-overhead-when-off
|
|
160
|
+
|
|
161
|
+
- The off path is one global-property read plus an early return; the marker `Error`, descriptor object, and message resolution happen only on the materialized or failing path.
|
|
162
|
+
- The `hasHost` constant lets callers skip expensive pre-check work entirely when nothing is listening.
|
|
163
|
+
- A `() => string` message thunk avoids building an expensive message unless it is needed.
|
|
164
|
+
- `check` is a plain named import with a static, recognizable signature, so an unassert-style AST transform can delete the calls entirely in release builds.
|
|
165
|
+
|
|
166
|
+
## Scope
|
|
167
|
+
|
|
168
|
+
The API is deliberately minimal — predicate-only. The equality family (`deepEqual`, `match`, …) is **out of scope**: deep comparison needs an engine, which would force either a dependency or a silent shallow downgrade in production (the same invariant would mean different things in test vs. prod). A richer package can come later, forming richer descriptors for `reportAssertion`; this one stays predicate-only and zero-dependency.
|
|
169
|
+
|
|
170
|
+
## Notes
|
|
171
|
+
|
|
172
|
+
- ES modules only (`"type": "module"`); no CommonJS build, no transpilation.
|
|
173
|
+
- Library, not a CLI — no `bin`. The whole implementation is the single root module `index.js`.
|
|
174
|
+
- TypeScript types live in `index.d.ts` (the sole source of types and docs); `index.js` carries `// @ts-self-types="./index.d.ts"`.
|
package/llms.txt
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# tape-six-invariant
|
|
2
|
+
|
|
3
|
+
> Zero-dependency invariant checks that materialize into real tape-six assertions under test, and are inert (or configurable) in production. Works in Node, Deno, Bun, and browsers. Never imports tape-six — coordinates through the global slot `Symbol.for('tape6.invariant.host.v1')`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
npm i tape-six-invariant
|
|
8
|
+
|
|
9
|
+
## Quick start
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import check from 'tape-six-invariant';
|
|
13
|
+
|
|
14
|
+
export function transfer(from, to, amount) {
|
|
15
|
+
check(amount > 0, 'amount must be positive');
|
|
16
|
+
check(from.currency === to.currency, 'currencies must match');
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Under a tape-six run each `check()` becomes a counted assertion on the current test. In production it runs the configured absent behavior, if any; by default none is set, so a failing check is a no-op.
|
|
21
|
+
|
|
22
|
+
## API
|
|
23
|
+
|
|
24
|
+
### check(cond, message?)
|
|
25
|
+
|
|
26
|
+
Records an invariant. The default export, also available as a named export (`import check from 'tape-six-invariant'` or `import {check} from 'tape-six-invariant'`). TypeScript signature: `(cond: unknown, message?: string | (() => string)) => asserts cond`.
|
|
27
|
+
|
|
28
|
+
- `cond` — pass/fail verdict; a falsy value fails the invariant.
|
|
29
|
+
- `message` — failure message, or a `() => string` thunk resolved only when needed.
|
|
30
|
+
|
|
31
|
+
With a tape-six host present, reports `{ok, message, marker}` to the current test (source location at the call site). With no host, on failure runs the absent behavior; a passing check is a no-op.
|
|
32
|
+
|
|
33
|
+
### hasHost
|
|
34
|
+
|
|
35
|
+
`const hasHost: boolean` — import-time snapshot of whether a tape-six host was installed when the module loaded. Gate expensive pre-check computation: `if (hasHost) check(expensiveToCompute(), '…')`. Correctness never depends on it; `check` reads the live slot per call.
|
|
36
|
+
|
|
37
|
+
### setAbsentBehavior(fn)
|
|
38
|
+
|
|
39
|
+
Sets the absent-path behavior (what a failing `check` does with no host). `fn: (ok: unknown, message?: string) => void`. Passing `null` or any non-function clears it; the default is none (a failing check is a no-op). Module-scoped — each copy of the library configures itself.
|
|
40
|
+
|
|
41
|
+
### Canned behaviors
|
|
42
|
+
|
|
43
|
+
Exported functions to pass to `setAbsentBehavior` (the default is none — a no-op):
|
|
44
|
+
|
|
45
|
+
- `throwOnFail` — throws `InvariantError`.
|
|
46
|
+
- `warnOnFail` — `console.warn`.
|
|
47
|
+
|
|
48
|
+
To defer to `node:assert` (or any other assertion library), bring your own import and pass it to `setAbsentBehavior` — it stays your dependency, not the package's: `setAbsentBehavior((ok, message) => assert.ok(ok, message))`.
|
|
49
|
+
|
|
50
|
+
### InvariantError
|
|
51
|
+
|
|
52
|
+
`class InvariantError extends Error` — thrown by `throwOnFail`. `name` is `'InvariantError'`; default message `'Invariant failed'`.
|
|
53
|
+
|
|
54
|
+
## Coordination protocol
|
|
55
|
+
|
|
56
|
+
- tape-six installs `globalThis[Symbol.for('tape6.invariant.host.v1')] = {version, report(assertion)}` at module load (`||=`, first copy wins).
|
|
57
|
+
- `report` resolves the live current tester via tape-six's `getTester()` and forwards to `Tester.reportAssertion({ok, message?, marker?, operator?, expected?, actual?})`.
|
|
58
|
+
- A global (not a module variable) so duplicate library copies in a dependency graph share one coordination point. Each realm (worker, iframe, subprocess) has its own slot.
|
|
59
|
+
|
|
60
|
+
## Scope
|
|
61
|
+
|
|
62
|
+
Predicate-only and zero-dep by design. The equality family (`deepEqual`, `match`, …) is out of scope — deep comparison needs an engine, which would force a dependency or env-divergent semantics.
|
|
63
|
+
|
|
64
|
+
## Notes
|
|
65
|
+
|
|
66
|
+
- ES modules only; no build step; TypeScript bindings in `index.d.ts`.
|
|
67
|
+
- `check` is statically matchable so an unassert-style build transform can strip calls in release builds.
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tape-six-invariant",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zero-dependency invariant checks that materialize into real tape-six assertions under test, and stay inert (or configurable) in production.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"module": "index.js",
|
|
8
|
+
"types": "index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./index.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"lint": "prettier --check .",
|
|
14
|
+
"lint:fix": "prettier --write .",
|
|
15
|
+
"test": "tape6 --flags FO",
|
|
16
|
+
"test:bun": "tape6-bun --flags FO",
|
|
17
|
+
"test:deno": "tape6-deno --flags FO",
|
|
18
|
+
"ts-test": "tape6 --flags FO 'tests/test-*.ts'",
|
|
19
|
+
"ts-test:bun": "tape6-bun --flags FO 'tests/test-*.ts'",
|
|
20
|
+
"ts-test:deno": "tape6-deno --flags FO 'tests/test-*.ts'",
|
|
21
|
+
"ts-check": "tsc --noEmit",
|
|
22
|
+
"js-check": "tsc --project tsconfig.check.json"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/uhop/tape-six-invariant.git"
|
|
27
|
+
},
|
|
28
|
+
"keywords": [
|
|
29
|
+
"tape6",
|
|
30
|
+
"tape-six",
|
|
31
|
+
"invariant",
|
|
32
|
+
"invariants",
|
|
33
|
+
"assert",
|
|
34
|
+
"assertion",
|
|
35
|
+
"contract",
|
|
36
|
+
"design-by-contract",
|
|
37
|
+
"test",
|
|
38
|
+
"testing",
|
|
39
|
+
"unit-test",
|
|
40
|
+
"cross-runtime",
|
|
41
|
+
"nodejs",
|
|
42
|
+
"deno",
|
|
43
|
+
"bun",
|
|
44
|
+
"browser",
|
|
45
|
+
"esm",
|
|
46
|
+
"es-modules",
|
|
47
|
+
"typescript",
|
|
48
|
+
"zero-dependency"
|
|
49
|
+
],
|
|
50
|
+
"author": "Eugene Lazutkin <eugene.lazutkin@gmail.com> (https://www.lazutkin.com/)",
|
|
51
|
+
"funding": "https://github.com/sponsors/uhop",
|
|
52
|
+
"license": "BSD-3-Clause",
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/uhop/tape-six-invariant/issues"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/uhop/tape-six-invariant#readme",
|
|
57
|
+
"llms": "https://raw.githubusercontent.com/uhop/tape-six-invariant/main/llms.txt",
|
|
58
|
+
"llmsFull": "https://raw.githubusercontent.com/uhop/tape-six-invariant/main/llms-full.txt",
|
|
59
|
+
"files": [
|
|
60
|
+
"index.js",
|
|
61
|
+
"index.d.ts",
|
|
62
|
+
"llms.txt",
|
|
63
|
+
"llms-full.txt"
|
|
64
|
+
],
|
|
65
|
+
"tape6": {
|
|
66
|
+
"tests": [
|
|
67
|
+
"/tests/test-*.js"
|
|
68
|
+
],
|
|
69
|
+
"cli": [
|
|
70
|
+
"/tests/cli/test-*.*js"
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
"devDependencies": {
|
|
74
|
+
"@types/node": "^26.0.1",
|
|
75
|
+
"prettier": "^3.9.0",
|
|
76
|
+
"tape-six": "^1.11.0",
|
|
77
|
+
"typescript": "^6.0.3"
|
|
78
|
+
}
|
|
79
|
+
}
|