@bemedev/dev-utils
v1.5.0
Published
A collection of utilities for Node.js development, including build tools, testing utilities, and configuration management.
Maintainers
Readme
@bemedev/dev-utils
A collection of utilities for Node.js development, including build tools, testing utilities, and configuration management.
HOTFIX fix for version 1.0.*
It doesn't type declarations well, if you're not put directly the output type
Modules
| Module | Description |
| ----------------------------------- | -------------------------------------------- |
| vitestExclude | Vitest plugin for glob-based include/exclude |
| vitestExtended | Extended test helpers for Vitest |
| rolldownConfig | Rolldown bundler configuration factory |
| buildTests | CLI tooling to build and test packages |
| plugins | Vite development helper plugins |
| utils | Utility functions and helpers |
Note (v0.8.1): The
./vitest-aliasexport has been removed. Path alias resolution is now handled natively by Vite viaresolve.aliaswithvite-tsconfig-pathsor the built-inresolve.tsConfigPathsoption. Remove anyaliasTs()/createAlias()usage and configure aliases directly in yourvite/vitestconfig.
Installation
pnpm add @bemedev/dev-utils
# or
npm install @bemedev/dev-utils
# or
yarn add @bemedev/dev-utilsPeer dependencies: typescript ^5 || ^6
Usage
Each module is available as a dedicated sub-path export — there is no root barrel export:
import { exclude } from '@bemedev/dev-utils/vitest-exclude';
import { createTests, createFakeWaiter } from '@bemedev/dev-utils/vitest-extended';
import { defineConfig } from '@bemedev/dev-utils/rolldown';
import { addTarball, cleanup } from '@bemedev/dev-utils/build-tests';
import { suppressWarnings, hmr } from '@bemedev/dev-utils/plugins';
import { clamp, toArray, identity, sleep } from '@bemedev/dev-utils/utils';vitestExclude
Vitest plugin that uses glob patterns to dynamically build test.include and
coverage.include lists — no need to maintain test.exclude manually.
exclude(args?)
Uses default patterns (src/**/*.ts for coverage,
src/**/*.{test,spec}.{ts,js,tsx,jsx} for tests) and applies optional ignore lists.
import { exclude } from '@bemedev/dev-utils/vitest-exclude';
// vitest.config.ts
export default defineConfig({
plugins: [
exclude({
ignoreTestFiles: ['src/fixtures/**'],
ignoreCoverageFiles: ['src/**/*.types.ts'],
}),
],
});Parameters (args)
| Property | Type | Description |
| --------------------- | ----------------------- | -------------------------------------------- |
| ignoreTestFiles | string[] \| undefined | Glob patterns to exclude from test discovery |
| ignoreCoverageFiles | string[] \| undefined | Glob patterns to exclude from coverage |
Returns a Vitest Plugin.
exclude.withPattern(patterns, args)
Same as exclude, but lets you provide custom glob patterns instead of the defaults.
exclude.withPattern(
{ patternTest: 'tests/**/*.test.ts', patternCov: 'src/**/*.ts' },
{ ignoreTestFiles: ['tests/fixtures/**'] },
);Parameters
| Name | Type | Description |
| ------------- | -------------------- | ----------------------------------- |
| patternTest | string \| string[] | Glob(s) for test file discovery |
| patternCov | string \| string[] | Glob(s) for coverage file discovery |
| args | same as exclude | Optional ignore lists |
Returns a Vitest Plugin.
create(args?)
Plain async function (not a plugin) that resolves glob patterns and returns
{ files, coverage } string arrays using the default patterns.
import { create } from '@bemedev/dev-utils/vitest-exclude';
const { files, coverage } = await create({
ignoreTestFiles: ['src/fixtures/**'],
ignoreCoverageFiles: ['src/**/*.types.ts'],
});Parameters (args)
| Property | Type | Description |
| --------------------- | ----------------------- | -------------------------------------------- |
| ignoreTestFiles | string[] \| undefined | Glob patterns to exclude from test discovery |
| ignoreCoverageFiles | string[] \| undefined | Glob patterns to exclude from coverage |
Returns Promise<{ files: string[]; coverage: string[] }>.
create.withPattern(patterns, args)
Same as create, but accepts custom glob patterns for both test and coverage
discovery.
import { create } from '@bemedev/dev-utils/vitest-exclude';
const { files, coverage } = await create.withPattern(
{ patternTest: 'tests/**/*.test.ts', patternCov: 'src/**/*.ts' },
{ ignoreTestFiles: ['tests/fixtures/**'] },
);Parameters
| Name | Type | Description |
| ------------- | -------------------- | ----------------------------------- |
| patternTest | string \| string[] | Glob(s) for test file discovery |
| patternCov | string \| string[] | Glob(s) for coverage file discovery |
| args | same as create | Optional ignore lists |
Returns Promise<{ files: string[]; coverage: string[] }>.
vitestExtended
Extended helpers for writing structured, type-safe Vitest tests.
createTests(func, args?)
Creates a reusable test suite for a function with acceptation, success, and
fails runners.
import { createTests } from '@bemedev/dev-utils/vitest-extended';
// add.ts
export const add = (a: number, b: number) => a + b;
// add.test.ts
const { acceptation, success, fails } = createTests(add);
describe('add', () => {
describe('acceptation', acceptation());
describe(
'success',
success(
{ invite: '1 + 2 = 3', parameters: [1, 2], expected: 3 },
{ invite: '0 + 0 = 0', parameters: [0, 0], expected: 0 },
),
);
});Note: When the first parameter is an array, wrap it in another array:
parameters: [[1, 2, 3]].
Parameters
| Name | Type | Description |
| ---------------- | --------------------------- | --------------------------------------------------------------- |
| func | Fn | The function under test |
| args.transform | NextFn<F> \| undefined | Optional transformer applied to the return value |
| args.toError | ToError_F<F> \| undefined | Optional function that derives an expected error from the input |
Returns { acceptation, success, fails }:
| Method | Signature | Description |
| ------------------- | ------------ | ------------------------------------------------------ |
| acceptation() | () => void | Runs two checks: function is defined and is a function |
| success(...cases) | () => void | Runs test.concurrent.each for passing cases |
| fails(...cases) | () => void | Runs test.concurrent.each and expects thrown errors |
createTests.withImplementation(f, opts)
Same as createTests but replaces the implementation with a vi.fn() mock before
the test suite runs.
const { acceptation, success } = createTests.withImplementation(myFn, {
name: 'myFn',
instanciation: async () => realImpl,
});opts properties
| Property | Type | Description |
| --------------- | --------------------------- | -------------------------------------------------------- |
| instanciation | () => Promise<F> \| F | Factory called in beforeAll to set mock implementation |
| name | string | Display name used in acceptation tests |
| transform | NextFn<F> \| undefined | Optional return-value transformer |
| toError | ToError_F<F> \| undefined | Optional error deriver |
doneTest(invite, fn, options?)
Wraps a callback-style (done-based) test into a Vitest test. The test passes only
when done() is called within the timeout. Supports both sync and async test
functions.
import { doneTest } from '@bemedev/dev-utils/vitest-extended';
// Async example with await
doneTest('async callback', async done => {
await service.start();
await service.send('event');
});
// Sync example
doneTest('fires callback', done => {
emitter.once('event', done);
emitter.emit('event');
});Parameters
| Name | Type | Default | Description |
| --------- | --------------------------------------------- | ------- | ------------------------------------------ |
| invite | string | — | Test description |
| fn | (done: () => void) => void \| Promise<void> | — | Test body (sync or async, receives done) |
| options | number \| TestOptions | 100 | Timeout in ms or Vitest TestOptions |
Variants
| Variant | Description |
| ------------------------------------------- | -------------------------- |
| doneTest.fails(invite, fn, options?) | Expects the test to fail |
| doneTest.concurrent(invite, fn, options?) | Runs the test concurrently |
createFakeWaiter(vi)
Creates a fake-timer-aware async waiter. If fake timers are active it advances them;
otherwise it uses a real sleep.
import { createFakeWaiter } from '@bemedev/dev-utils/vitest-extended';
const wait = createFakeWaiter(vi);
test('advances time', async () => {
vi.useFakeTimers();
await wait(500); // advances fake timers by 500 ms
});Parameters
| Name | Type | Description |
| ---- | ------------- | ------------------ |
| vi | VitestUtils | Vitest vi object |
Returns (ms?: number, times?: number) => Promise<void>
Variants
| Variant | Signature | Description |
| -------------------------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------- |
| createFakeWaiter.withDefaultDelay(vi, ms?) | (index, times?) => [string, () => Promise<void>] | Returns a labelled waiter with a fixed delay |
| createFakeWaiter.all(vi) | (index, ms?, times?) => [string, () => Promise<void>] | Returns a labelled waiter with configurable delay and repetitions |
useEach(func, transform?)
A typed wrapper around test.concurrent.each for synchronous functions.
import { useEach } from '@bemedev/dev-utils/vitest-extended';
const run = useEach(add);
run(['1+2', [1, 2], 3], ['0+0', [0, 0], 0]);Parameters
| Name | Type | Description |
| ----------- | ------------------------ | --------------------------------- |
| func | Fn | Function under test |
| transform | NextFn<F> \| undefined | Optional return-value transformer |
Returns (...cases: TestArgs2<F>) => void
useEachAsync(func, transform?)
Same as useEach but awaits the function result.
useErrorAsyncEach(func, transform?, toError?)
Runs async error cases using expect(fn).rejects.toThrowError(error).
const runErrors = vitestExtended.useErrorAsyncEach(divide, undefined, ([_, b]) =>
b === 0 ? 'Division by zero' : undefined,
);
runErrors(['divide by 0', [1, 0], undefined]);useTestFunctionAcceptation(f, name?) / useTFA
Runs two acceptation checks: the function is defined and is actually a function.
vitestExtended.useTFA(myFunction, 'myFunction');defineProject(config)
A wrapper around Vitest's defineProject / defineConfig that injects default
configurations.
[!IMPORTANT] HOT FIX: Use this instead of standard
defineProjectfromvitest/configto automatically resolve path aliases (viatsconfigPaths: true), setglobals: true, configurelogHeapUsage: false, and set theNODE_ENVenvironment variable to'test'by default.
import { defineProject } from '@bemedev/dev-utils/vitest-extended';
export default defineProject({
test: {
// your config here
},
});Utility helpers
| Export | Signature | Description |
| ---------------------------------- | ----------------------------------- | ------------------------------------------------- |
| isDefined(value?) | (value?: T \| null) => value is T | Type guard — not undefined or null |
| isFunction(arg) | (arg: any) => boolean | Type guard — checks if arg is a callable function |
| identity(value) | <T>(value: T) => T | Returns the value unchanged |
| defaultEquality(value, expected) | (a, b) => void | Strict equality; sorts arrays before comparing |
rolldownConfig
Rolldown bundler configuration factory with built-in plugins for TypeScript declarations, circular dependency detection, and tree-shaking externals.
defineConfig(additionals?)
Shorthand for defineConfig.default.
defineConfig.default(params?)
Produces a full Rolldown config with the default plugin pipeline:
- circulars — warns on circular dependencies
(
rollup-plugin-circular-dependency) - externals — marks
dependenciesandpeerDependenciesas external (rollup-plugin-node-externals) - typescript — emits
.d.tsdeclarations via the TypeScript compiler API - clean — removes JS outputs for files that produce empty chunks
// rolldown.config.ts
import { defineConfig } from '@bemedev/dev-utils/rolldown';
export default defineConfig.default({
dir: 'lib',
sourcemap: true,
excludesTS: ['src/fixtures/**/*.ts'],
});Params properties
| Property | Type | Default | Description |
| ---------------- | --------------------------------------- | ------------- | ------------------------------------------------------- |
| dir | string | 'lib' | Output directory |
| sourcemap | boolean | false | Emit sourcemaps |
| declarationMap | boolean | undefined | Emit declaration maps |
| excludesTS | string \| string[] | [] | Extra TS files to exclude from declaration emit |
| circularDeps | string \| string[] | [] | Extra patterns exempt from circular-dep warnings |
| ignoresJS | string \| string[] | [] | Patterns whose empty-bundle warnings are suppressed |
| externals | string \| string[] | [] | Additional packages to mark as external |
| plugins | (RolldownPluginOption \| PluginKey)[] | default order | Custom plugin list; string keys select built-in plugins |
Default excluded files (always applied):
**/node_modules/**/*
**/__tests__/**/*
**/*.test.ts / **/*.test-d.ts
**/*.fixtures.ts / **/fixtures.ts
src/fixtures/**/*.tsOutput format: both esm (.js) and cjs (.cjs), with
preserveModules: true.
defineConfig.bemedev(params?)
Opinionated preset that extends defineConfig.default with:
- Sourcemaps enabled by default
DEFAULT_CIRCULAR_DEPSmerged intocircularDeps(**/types.ts,**/*.types.ts, …)DEFAULT_EXCLUDEmerged intoexcludesTSDEFAULT_CIRCULAR_DEPSmerged intoignoresJS
export default defineConfig.bemedev({ excludesTS: ['src/my-fixtures/**'] });defineConfig.fast(params?)
A fast build configuration preset that optimizes TypeScript declaration emitting. It
uses the fast (typescriptFast) plugin wrapping oxc-transform instead of the
standard compiler API.
It runs the following plugin pipeline:
- circulars — warns on circular dependencies
- externals — marks
dependenciesandpeerDependenciesas external - esm — injects ESM external require helpers (
esmExternalRequirePlugin) - fast — generates
.d.tsfiles quickly usingoxc-transform - clean — removes empty JS chunks
export default defineConfig.fast({ excludesTS: ['src/my-fixtures/**'] });Plugin builders (PLUGIN_BUILDERS)
Individual plugin factories are available for custom pipeline assembly:
| Key | Function | Wraps |
| ------------ | ---------------------- | ------------------------------------------------------------------------------------------ |
| circulars | circulars(options?) | rollup-plugin-circular-dependency |
| externals | externals(options?) | rollup-plugin-node-externals |
| esm | esm | esmExternalRequirePlugin from Rolldown |
| typescript | typescript(options?) | TypeScript compiler API + tsc-alias |
| fast | fast(options?) | oxc-transform + tsc-alias, for ONLY compilerOptions.isolatedDeclarations set to true |
| clean | clean(options?) | Custom post-build JS cleanup |
Pass plugin keys as strings in the plugins array to reorder or subset them:
defineConfig.default({ plugins: ['externals', 'typescript'] });buildTests
CLI tooling that packs your built library as a local tarball, installs it under the
alias this-gen-1, runs your test suite against the real distributable, then tears
down.
Available as a sub-path export:
import {
addTarball,
cleanup,
customImport,
THIS1,
} from '@bemedev/dev-utils/build-tests';CLI usage
The binary is build-tests. It exposes three subcommands:
# Install the tarball (pre-test hook)
build-tests pre
# Run tests with pre/post lifecycle
build-tests test [--pretest] [--posttest]
# Clean up tarball + uninstall (post-test hook)
build-tests postpackage.json integration (auto-configured):
{
"scripts": {
"pretest": "build && build-tests pre",
"posttest": "fmt && build-tests post"
}
}addTarball()
Programmatic API for the pre step:
- Builds the
package.jsonfor theoutDir(stripsscripts,devDependencies,files; fixes paths) - Runs
pnpm packinto.pack/ - Installs the resulting
.tgzasthis-gen-1indevDependencies
import { addTarball } from '@bemedev/dev-utils/build-tests';
await addTarball();cleanup()
Programmatic API for the post step:
- Removes the
.pack/folder - Runs
pnpm remove this-gen-1
import { cleanup } from '@bemedev/dev-utils/build-tests';
cleanup();customImport(fn?)
Dynamically imports the installed package (this-gen-1) and optionally transforms
its exports via a mapper function.
import { customImport } from '@bemedev/dev-utils/build-tests';
// Import the default export as-is
const out = await customImport();
// Import and pick a specific export
const myFn = await customImport(mod => mod.myFn);Parameters
| Name | Type | Default | Description |
| ---- | ------------------------- | ---------- | -------------------------------------------------------- |
| fn | (out: IndexImport) => T | identity | Mapper applied to the imported module; defaults to no-op |
Returns Promise<T> — the mapped module output.
THIS1
Constant string 'this-gen-1' — the package alias used by addTarball and cleanup
when installing/removing the local tarball.
import { THIS1 } from '@bemedev/dev-utils/build-tests';
console.log(THIS1); // 'this-gen-1'Types
| Name | Description |
| ------------- | --------------------------------------------------------------------- |
| IndexImport | Type of the module returned by a dynamic import(THIS1) call (any) |
plugins
Vite plugins for development and test workflows.
suppressWarnings(...patterns)
Vite plugin that intercepts and suppresses noisy terminal stderr output (e.g., from
Nitro, SSR workers, or third-party dependencies) matching specific strings or regular
expressions during dev (apply: 'serve').
import { suppressWarnings } from '@bemedev/dev-utils/plugins';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [suppressWarnings('ExperimentalWarning', /punycode/, '[Vue warn]')],
});Parameters
| Name | Type | Description |
| ------------- | ------------------------- | --------------------------------------------------------------------- |
| ...patterns | Array<string \| RegExp> | Strings or RegExp patterns to match against stderr chunks to suppress |
Returns a Vite Plugin ({ name: 'suppress-logs-plugin', apply: 'serve' }).
hmr(props?)
Vite plugin providing Hot Module Reloading (HMR) capabilities for external file paths by watching files and executing mapped build scripts when changes are detected.
import { hmr } from '@bemedev/dev-utils/plugins';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [
hmr({
paths: { '../packages/*/src/**': 'pnpm --filter ./packages/* build' },
debounce: 1000,
}),
],
});Parameters (props)
| Property | Type | Default | Description |
| ---------- | ------------------------------------ | ----------- | ---------------------------------------------------------------------- |
| paths | Record<string, string \| string[]> | undefined | Map of watched path patterns to command(s) to execute on file changes |
| debounce | number \| undefined | 500 | Cooldown interval in milliseconds to prevent build loops (500–10000ms) |
Returns a Vite Plugin ({ name: 'hmr-add-on' }).
utils
General development utility helpers available via the ./utils sub-path export.
clamp(value, min, max)
Restricts a number between a minimum and maximum boundary.
import { clamp } from '@bemedev/dev-utils/utils';
clamp(5, 0, 10); // 5
clamp(-5, 0, 10); // 0
clamp(15, 0, 10); // 10toArray(value)
Normalizes a single value, array, null, or undefined into an array.
import { toArray } from '@bemedev/dev-utils/utils';
toArray('foo'); // ['foo']
toArray(['a', 'b']); // ['a', 'b']
toArray(undefined); // []
toArray(null); // []identity(value)
Returns the passed argument unchanged.
import { identity } from '@bemedev/dev-utils/utils';
identity('test'); // 'test'sleep(ms)
Asynchronously pauses execution for a given number of milliseconds.
import { sleep } from '@bemedev/dev-utils/utils';
await sleep(1000); // waits 1 secondLicence
MIT
CHANGELOG
Read CHANGELOG.md for details about all changes and versions.
Author
chlbri ([email protected])
