@paulmillr/jsbt
JS Build Tools
Zero-dependency helpers for secure JS apps, used by noble cryptography.
- test 500-line simplicity with mocha-like syntax and parallelism
- benchmark with nanosecond resolution
- CLI to check project for common mistakes
- workflows for GitHub CI actions for test / npm+jsr publish
- tsconfig with strict, doc-friendly, with type stripping
Usage
npm install @paulmillr/jsbt
jsr add jsr:@paulmillr/jsbt
1. test
Small test runner with familiar describe / it mocha-like syntax, explicit execution, and
optional parallelism.
API:
it(title, fn)register sync or async tests.describe(title, fn)groups tests and scopesbeforeEach/afterEach.it.only(title, fn)runs one test;should.skip(title, fn)reports a skipped test.it.serial(title, fn)keeps a test on the main process when fast mode is enabled.it.run()runs the current file's registered tests.it.runWhen(import.meta.url)runs only when the file was launched directly, which keeps imported subtests from running twice in aggregate test files.
ENV variables:
JSBT_BAIL=0disables stopping after the first failed test (1by default).JSBT_FAST=1enables parallel execution with all available cores.JSBT_FAST=3uses three workers.JSBT_FAST=-1uses all cores minus one.JSBT_FAST=0.5uses half of available cores.JSBT_QUIET=1enables the quiet (dot) reporter.JSBT_FILTER=math/addsruns tests whose full path contains the value.
import { deepStrictEqual } from 'node:assert';
import { beforeEach, describe, it } from '@paulmillr/jsbt/test.js';
describe('math', () => {
let value = 0;
beforeEach(() => {
value = 2;
});
it('adds', () => {
deepStrictEqual(value + 2, 4);
});
it('works with async code', async () => {
deepStrictEqual(await Promise.resolve(value * 3), 6);
});
it.skip('documents known gaps without running them', () => {
deepStrictEqual(true, false);
});
});
await should.runWhen(import.meta.url);
Run a project test entrypoint with node:
node test/index.ts
JSBT_FAST=1 node test/index.ts
JSBT_QUIET=1 node test/index.ts
JSBT_FILTER=math/adds node test/index.ts
When launched by node --test, jsbt/test registers suites, hooks, and test cases with
Node's native runner instead of printing its own report. Existing it.run() and
it.runWhen(import.meta.url) calls become harmless in that mode, so the same files can be run
directly or through Node's test runner.
2. benchmark
Lightweight benchmark helpers with nanosecond timing, terminal-friendly output, throughput units, and a matrix runner for comparing libraries, algorithms, platforms, input sizes, and other dimensions.
benchmark
Use bench for simple one-line measurements:
import bench from '@paulmillr/jsbt/benchmark.js';
const data = new Uint8Array(1024 * 1024);
const processBlock = () => data[0];
await bench('sqrt', () => Math.sqrt(2));
await bench('copy 1MiB', () => data.slice(), { bytes: data.byteLength });
await bench('blocks', () => processBlock(), { throughput: { amount: 16, unit: 'blocks' } });
Options:
bytes: bytes processed by one benchmark iteration; output isb/sec,kib/sec,mib/sec, orgib/sec.throughput: custom units processed by one iteration, for example{ amount: 16, unit: 'blocks' }.maxRunTimeSec: per-benchmark runtime, from0.1to60seconds; defaults to0.4.mode: 'runOnce': run one measurement and print only elapsed time.section('math')named export: print# mathin text output and prefix CSV names asmath; <name>.section()orsection('')disables the prefix.JSBT_CSV=1forces CSV output. CSV printsname,nanosecondsby default, orname,<unit>/secforbytesandthroughput, and is also the default when color output is disabled.
Example output:
sqrt x 6,072 ops/sec @ 164μs/op
copy 1MiB x 1,420 mib/sec
blocks x 92,400 blocks/sec
benchmark-compare
Use benchmark-compare for benchmark matrices. Static dimensions provide benchmark arguments; nested
library objects provide dynamic dimensions.
import compare from '@paulmillr/jsbt/benchmark-compare.js';
const sizes = {
'1KB': new Uint8Array(1024),
'1MiB': new Uint8Array(1024 * 1024),
};
const libraries = {
js: (buf) => buf.slice(),
native: (buf) => Buffer.from(buf),
};
await compare('copy', { size: sizes }, libraries, {
bytes: ({ args }) => args[0].byteLength,
});
Common options:
libraryDimensions: names for nested library levels; defaults to['name'].defaults: fixed dimension values that should not vary in the table.dimensions: explicit dimension order and subset.filter: comma-separated match terms;a|b,cmeans(a or b) and c.filterObj: predicate for filtering generated benchmark cases.iterations: repeats one measured operation and reports per-iteration timing.patchArgs: rewrites generated benchmark arguments before calling a library function.bytes,throughput,metrics: add throughput or custom metric columns.loadRun,skipThreshold,printUnchanged: compare against a saved previous run.format:tableorcsv; table is the default when colors are enabled, CSV otherwise.
ENV variables:
JSBT_FILTER=sha256,1MiBfilters cases by dimension values.JSBT_BENCHMARK_DIMENSIONS=algorithm,size,namechanges dimension order or visible dimensions.JSBT_BENCHMARK_DRY_RUN=1prints the selected matrix without measuring.JSBT_CSV=1forces CSV output.
3. CLI
jsbt-check CLI executes audit helpers.
check
Runs opinionated code quality checks. Uses typescript parsing underneath. Temporary build artifacts are created in a per-run OS temp directory and removed after the summary.
Example-running checks (readme, tsdoc, errors) execute examples in an isolated temp run
directory. Its node_modules is assembled from symlinks — nothing is fetched at check time:
- the checked package itself, installed under its own name;
- the package's runtime
dependencies, linked from the project's installednode_modules; - extra example-only imports allowed by
exampleDependenciesin a committed.jsbtrc.jsonbesidepackage.json, pinned to exact installed versions:
{
"exampleDependencies": {
"micro-packed": "0.7.3"
}
}
esbuild (used by treeshake) is provided automatically and must not be listed: it is
resolved from the project's node_modules, from the copy next to jsbt itself, or from a
global install. If none is found, run npm install -g esbuild.
jsbt-check [--project=<directory>]
jsbt-check [--project=<directory>] bigint
jsbt-check [--project=<directory>] bytes
jsbt-check [--project=<directory>] comments
jsbt-check [--project=<directory>] errors
jsbt-check [--project=<directory>] importtime
jsbt-check [--project=<directory>] jsr
jsbt-check [--project=<directory>] jsrpublish
jsbt-check [--project=<directory>] mutate
jsbt-check [--project=<directory>] patterns
jsbt-check [--project=<directory>] readme
jsbt-check [--project=<directory>] treeshake
jsbt-check [--project=<directory>] tsdoc
jsbt-check [--project=<directory>] typeimport
With "check": "jsbt-check" in package.json scripts, selectors can be run through npm:
npm run check bigint
npm run check bytes
npm run check comments
npm run check errors
npm run check importtime
npm run check jsr
npm run check jsrpublish
npm run check mutate
npm run check patterns
npm run check readme
npm run check treeshake
npm run check tsdoc
npm run check typeimport
Selector summary for jsbt-check <selector>:
bigint: find BigInt compatibility hazards in public runtime files.bytes: inspect byte/typed-array API surface and TypeScript-version compatibility.comments: enforce comments and release-facing source annotations.errors: verify documented thrown errors against runtime probes.importtime: measure public entry import time and flag slow imports.jsr: validate JSR package metadata, exports, imports, and publish graph.jsrpublish: run stricter JSR publish-readiness checks.mutate: detect mutation hazards in public runtime behavior.patterns: report source patterns that are risky for published packages.readme: type-check and run runnable README examples.treeshake: bundle public exports and report retained unused code.tsdoc: audit public declaration docs and examples.typeimport: verify imports that should be type-only.
4. Workflows
Secure GitHub CI configs for testing & publishing JS packages.
The files reside in .github/workflows:
test.yml: reusable/manual test workflow for Node 22, 24, 26, Bun, and Deno. It runsnpm run build --if-present,npm test, optionaltest:tscon Node 26, optionaltest:bun, and optionaltest:deno. Inputs:submodulesandruns-on.test-matrix.yml: reusable/manual Node matrix across Node 22, 24, 26 onubuntu-24.04-arm,macos-latest, andwindows-latest.test-custom.yml: reusable Node 26 workflow for one custom npm task, defaulting totest:slow.release.yml: release/reusable/manual publisher for NPM, and JSR whenjsr.jsonexists. It uses OIDC Trusted Publishing, disables package-manager cache, runsnpm ci, builds when present, verifies package/tag versions, dry-runs NPM publish, validates JSR version, and publishes throughnpm stage publish --access public.
You can copy them, or depend on them directly:
name: jsbt 0.5.2
on:
push:
pull_request:
jobs:
test:
uses: paulmillr/jsbt/.github/workflows/test.yml@0.5.2
For releases, configure NPM Trusted Publishing for the package first:
name: Publish release
on:
release:
types: [created]
jobs:
publish:
uses: paulmillr/jsbt/.github/workflows/release.yml@0.5.2
permissions:
contents: read
id-token: write
5. tsconfig
Strict typescript v6+ configs, friendly to type stripping. Uses isolatedDeclarations and verbatimModuleSyntax
to ensure node.js is able to natively run typescript files without compilation.
There are two files: tsconfig.json and tsconfig.test.json (looser, for tests).
Inheritable in the following way:
{
"extends": "@paulmillr/jsbt/tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "."
},
"include": [
"src"
],
"exclude": [
"node_modules"
]
}
License
MIT License