Skip to content

Commit ff2f7f2

Browse files
committed
feat(from-assert): add codemod to migrate node:assert to bupkis
Implements @bupkis/from-assert, a codemod tool to migrate node:assert assertions to bupkis. Supports both strict mode (node:assert/strict) and legacy mode (node:assert) assertions. Key features: - CLI tool (`bupkis-from-assert`) and programmatic API - Transforms all standard node:assert methods to bupkis equivalents - Handles async assertions (`rejects`, `doesNotReject`) with `expectAsync` - Detects legacy vs strict mode imports and warns about behavior changes - Returns warnings for unsupported methods like `ifError` Closes #328
1 parent ca4b516 commit ff2f7f2

24 files changed

+3042
-5
lines changed

.release-please-manifest.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
{
22
"packages/bupkis": "0.18.1",
33
"packages/events": "0.2.0",
4+
"packages/from-assert": "0.0.0",
45
"packages/from-chai": "0.1.0",
56
"packages/from-jest": "0.3.0",
7+
"packages/http": "0.1.1",
8+
"packages/msw": "0.1.0",
69
"packages/property-testing": "0.1.1",
710
"packages/rxjs": "0.1.0",
8-
"packages/sinon": "0.2.0",
9-
"packages/http": "0.1.1",
10-
"packages/msw": "0.1.0"
11+
"packages/sinon": "0.2.0"
1112
}

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ This is the monorepo for [**BUPKIS**][docs], the _uncommonly extensible assertio
2626

2727
### Migration Tools
2828

29+
- **[@bupkis/from-assert](./packages/from-assert/)** - Codemod to migrate [node:assert][] assertions to **BUPKIS**
2930
- **[@bupkis/from-chai](./packages/from-chai/)** - Codemod to migrate [Chai][] assertions to **BUPKIS**
3031
- **[@bupkis/from-jest](./packages/from-jest/)** - Codemod to migrate [Jest][]/[Vitest][] assertions to **BUPKIS**
3132

@@ -54,3 +55,4 @@ Copyright © 2025 [Christopher "boneskull" Hiller][boneskull]. Licensed under [B
5455
[Jest]: https://jestjs.io
5556
[Vitest]: https://vitest.dev
5657
[Chai]: https://www.chaijs.com
58+
[node:assert]: https://nodejs.org/api/assert.html

package-lock.json

Lines changed: 22 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/from-assert/LICENSE.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Blue Oak Model License
2+
3+
Version 1.0.0
4+
5+
## Purpose
6+
7+
This license gives everyone as much permission to work with
8+
this software as possible, while protecting contributors
9+
from liability.
10+
11+
## Acceptance
12+
13+
In order to receive this license, you must agree to its
14+
rules. The rules of this license are both obligations
15+
under that agreement and conditions to your license.
16+
You must not do anything with this software that triggers
17+
a rule that you cannot or will not follow.
18+
19+
## Copyright
20+
21+
Each contributor licenses you to do everything with this
22+
software that would otherwise infringe that contributor's
23+
copyright in it.
24+
25+
## Notices
26+
27+
You must ensure that everyone who gets a copy of
28+
any part of this software from you, with or without
29+
changes, also gets the text of this license or a link to
30+
<https://blueoakcouncil.org/license/1.0.0>.
31+
32+
## Excuse
33+
34+
If anyone notifies you in writing that you have not
35+
complied with [Notices](#notices), you can keep your
36+
license by taking all practical steps to comply within 30
37+
days after the notice. If you do not do so, your license
38+
ends immediately.
39+
40+
## Patent
41+
42+
Each contributor licenses you to do everything with this
43+
software that would otherwise infringe any patent claims
44+
they can license or become able to license.
45+
46+
## Reliability
47+
48+
No contributor can revoke this license.
49+
50+
## No Liability
51+
52+
**_As far as the law allows, this software comes as is,
53+
without any warranty or condition, and no contributor
54+
will be liable to anyone for any damages related to this
55+
software or this license, under any kind of legal claim._**

packages/from-assert/README.md

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
# @bupkis/from-assert
2+
3+
Migrate `node:assert` assertions to [bupkis](https://github.com/boneskull/bupkis) with a single command.
4+
5+
Supports both **strict mode** (`node:assert/strict`) and **legacy mode** (`node:assert`) assertions.
6+
7+
## Installation
8+
9+
```bash
10+
npx @bupkis/from-assert
11+
```
12+
13+
Or install locally:
14+
15+
```bash
16+
npm install -D @bupkis/from-assert
17+
```
18+
19+
## Usage
20+
21+
### CLI
22+
23+
```bash
24+
# Transform all test files in current directory
25+
npx bupkis-from-assert
26+
27+
# Transform specific patterns
28+
npx bupkis-from-assert "src/**/*.test.ts" "test/**/*.spec.ts"
29+
30+
# Dry run (see what would change without writing)
31+
npx bupkis-from-assert --dry-run
32+
33+
# Exclude patterns
34+
npx bupkis-from-assert --exclude "**/fixtures/**"
35+
36+
# Strict mode (fail on any unsupported assertion)
37+
npx bupkis-from-assert --strict
38+
```
39+
40+
### Programmatic API
41+
42+
```typescript
43+
import { transform, transformCode } from '@bupkis/from-assert';
44+
45+
// Transform a code string
46+
const { code, transformCount, warnings } = await transformCode(`
47+
import assert from 'node:assert';
48+
assert.strictEqual(foo, bar);
49+
`);
50+
// Result: "import { expect } from 'bupkis';\nexpect(foo, 'to be', bar);"
51+
52+
// Transform files
53+
const result = await transform({
54+
include: ['**/*.test.ts'],
55+
exclude: ['**/node_modules/**'],
56+
mode: 'best-effort',
57+
write: true,
58+
});
59+
```
60+
61+
## Transformations
62+
63+
### Strict Equality
64+
65+
| node:assert | bupkis |
66+
| --------------------------------- | ----------------------------------- |
67+
| `assert.strictEqual(a, b)` | `expect(a, 'to be', b)` |
68+
| `assert.notStrictEqual(a, b)` | `expect(a, 'not to be', b)` |
69+
| `assert.deepStrictEqual(a, b)` | `expect(a, 'to deep equal', b)` |
70+
| `assert.notDeepStrictEqual(a, b)` | `expect(a, 'not to deep equal', b)` |
71+
72+
### Legacy Equality (with warning)
73+
74+
When using plain `node:assert` (legacy mode), loose equality assertions are transformed with a warning since behavior may differ:
75+
76+
| node:assert | bupkis |
77+
| --------------------------- | ----------------------------------- |
78+
| `assert.equal(a, b)` | `expect(a, 'to be', b)` ⚠️ |
79+
| `assert.notEqual(a, b)` | `expect(a, 'not to be', b)` ⚠️ |
80+
| `assert.deepEqual(a, b)` | `expect(a, 'to deep equal', b)` |
81+
| `assert.notDeepEqual(a, b)` | `expect(a, 'not to deep equal', b)` |
82+
83+
### Truthiness
84+
85+
| node:assert | bupkis |
86+
| ------------------ | ------------------------------- |
87+
| `assert(value)` | `expect(value, 'to be truthy')` |
88+
| `assert.ok(value)` | `expect(value, 'to be truthy')` |
89+
90+
### Throws
91+
92+
| node:assert | bupkis |
93+
| ---------------------------- | --------------------------------- |
94+
| `assert.throws(fn)` | `expect(fn, 'to throw')` |
95+
| `assert.throws(fn, Error)` | `expect(fn, 'to throw', Error)` |
96+
| `assert.throws(fn, /regex/)` | `expect(fn, 'to throw', /regex/)` |
97+
| `assert.doesNotThrow(fn)` | `expect(fn, 'not to throw')` |
98+
99+
### Async Assertions
100+
101+
Async assertions use `expectAsync`:
102+
103+
| node:assert | bupkis |
104+
| -------------------------------- | ----------------------------------------------- |
105+
| `assert.rejects(asyncFn)` | `expectAsync(asyncFn, 'to reject')` |
106+
| `assert.rejects(asyncFn, Error)` | `expectAsync(asyncFn, 'to reject with', Error)` |
107+
| `assert.doesNotReject(asyncFn)` | `expectAsync(asyncFn, 'not to reject')` |
108+
109+
### String Matching
110+
111+
| node:assert | bupkis |
112+
| ----------------------------------- | -------------------------------------- |
113+
| `assert.match(str, /regex/)` | `expect(str, 'to match', /regex/)` |
114+
| `assert.doesNotMatch(str, /regex/)` | `expect(str, 'not to match', /regex/)` |
115+
116+
### Fail
117+
118+
| node:assert | bupkis |
119+
| ---------------------- | ---------------------- |
120+
| `assert.fail()` | `expect.fail()` |
121+
| `assert.fail(message)` | `expect.fail(message)` |
122+
123+
### Unsupported
124+
125+
The following require manual migration:
126+
127+
- `assert.ifError(value)` - Semantics don't map cleanly
128+
- `assert.CallTracker` - Deprecated; use `@bupkis/sinon` instead
129+
- Complex error matchers with validation functions
130+
131+
## Import Handling
132+
133+
The transformer automatically handles all `node:assert` import styles:
134+
135+
```typescript
136+
// All of these are transformed
137+
import assert from 'node:assert';
138+
import assert from 'node:assert/strict';
139+
import assert from 'assert';
140+
import assert from 'assert/strict';
141+
import { strict as assert } from 'node:assert';
142+
```
143+
144+
The import is replaced with:
145+
146+
```typescript
147+
import { expect } from 'bupkis';
148+
// or, if async assertions are present:
149+
import { expect, expectAsync } from 'bupkis';
150+
```
151+
152+
## Options
153+
154+
| Option | Description |
155+
| ----------------- | ---------------------------------------------------------------- |
156+
| `--dry-run` | Show what would change without writing files |
157+
| `--strict` | Fail on any unsupported assertion |
158+
| `--best-effort` | Transform what we can, return warnings for unsupported (default) |
159+
| `--exclude`, `-e` | Patterns to exclude (default: `**/node_modules/**`) |
160+
161+
## Handling Unsupported Assertions
162+
163+
In `best-effort` mode (default), unsupported assertions are left unchanged and a warning is returned. Search for warnings in the output to find items requiring manual review.
164+
165+
## License
166+
167+
[Blue Oak Model License 1.0.0](https://blueoakcouncil.org/license/1.0.0)

packages/from-assert/doc/readme.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
title: '@bupkis/from-assert'
3+
category: Migration Tools
4+
---
5+
6+
{@include ../README.md}

packages/from-assert/package.json

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"name": "@bupkis/from-assert",
3+
"version": "0.0.0",
4+
"type": "module",
5+
"description": "Migrate node:assert assertions to bupkis",
6+
"repository": {
7+
"directory": "packages/from-assert",
8+
"type": "git",
9+
"url": "git+https://github.com/boneskull/bupkis.git"
10+
},
11+
"homepage": "https://bupkis.zip",
12+
"author": {
13+
"email": "[email protected]",
14+
"name": "Christopher Hiller"
15+
},
16+
"license": "BlueOak-1.0.0",
17+
"engines": {
18+
"node": "^20.19.0 || ^22.12.0 || >=23"
19+
},
20+
"bin": {
21+
"bupkis-from-assert": "./dist/cli.js"
22+
},
23+
"exports": {
24+
".": {
25+
"import": "./dist/index.js",
26+
"types": "./dist/index.d.ts"
27+
},
28+
"./package.json": "./package.json"
29+
},
30+
"files": [
31+
"CHANGELOG.md",
32+
"dist",
33+
"src"
34+
],
35+
"keywords": [
36+
"bupkis",
37+
"assert",
38+
"node:assert",
39+
"codemod",
40+
"migration",
41+
"assertions"
42+
],
43+
"scripts": {
44+
"build": "tsc",
45+
"test": "npm run test:base -- \"test/**/*.test.ts\"",
46+
"test:base": "node --test --test-reporter=spec --import tsx",
47+
"test:dev": "npm run test:base -- --watch \"test/**/*.test.ts\"",
48+
"test:node20": "npm run test:base -- test/**/*.test.ts"
49+
},
50+
"dependencies": {
51+
"@boneskull/bargs": "4.0.0",
52+
"ts-morph": "^25.0.0"
53+
},
54+
"devDependencies": {},
55+
"publishConfig": {
56+
"access": "public",
57+
"registry": "https://registry.npmjs.org/"
58+
},
59+
"prettier": {
60+
"jsdocCommentLineStrategy": "keep",
61+
"jsdocPreferCodeFences": true,
62+
"plugins": [
63+
"prettier-plugin-jsdoc",
64+
"prettier-plugin-pkg"
65+
],
66+
"singleQuote": true,
67+
"tsdoc": true
68+
}
69+
}

0 commit comments

Comments
 (0)