Skip to content

Commit 1b5d222

Browse files
satoshai-devclaude
andauthored
fix: handle jiti MODULE_NOT_FOUND error and update docs (#44)
* fix: handle jiti MODULE_NOT_FOUND error and update docs - isFileNotFound now treats MODULE_NOT_FOUND (from jiti) as file-not-found, so `abi-cli sync` without a config file shows a clean error message - README updated to document name alias, --check flag, barrel file generation, and corrected generated output format (as const satisfies ClarityAbi) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: add changeset for MODULE_NOT_FOUND fix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9f0976c commit 1b5d222

File tree

4 files changed

+74
-10
lines changed

4 files changed

+74
-10
lines changed

.changeset/fix-module-not-found.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@satoshai/abi-cli": patch
3+
---
4+
5+
Fix jiti MODULE_NOT_FOUND error when running sync without a config file

README.md

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,18 @@ abi-cli fetch SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01
3030
The generated file looks like:
3131

3232
```typescript
33+
import type { ClarityAbi } from '@stacks/transactions';
34+
3335
// ABI for SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01
3436
// Generated by @satoshai/abi-cli
3537

3638
export const abi = {
3739
"functions": [...],
3840
"variables": [...],
3941
...
40-
} as const;
42+
} as const satisfies ClarityAbi;
43+
44+
export type Abi = typeof abi;
4145
```
4246

4347
### Output to stdout
@@ -100,13 +104,15 @@ Create `abi.config.json` in your project root:
100104
"format": "ts",
101105
"network": "mainnet",
102106
"contracts": [
103-
{ "id": "SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01" },
104-
{ "id": "SP2C2YFP12AJZB1KD5HQ4XFRYGEK02H70HVK8GQH.arkadiko-swap-v2-1" },
107+
{ "id": "SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01", "name": "amm-pool" },
108+
{ "id": "SP2C2YFP12AJZB1KD5HQ4XFRYGEK02H70HVK8GQH.arkadiko-swap-v2-1", "name": "arkadiko-swap" },
105109
{ "id": "ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM.my-contract", "network": "testnet" }
106110
]
107111
}
108112
```
109113

114+
The `name` field controls the output filename. This decouples your imports from the on-chain contract ID — when you upgrade to a new contract version, change the `id` but keep the `name`. Your imports stay the same.
115+
110116
Or use `abi.config.ts` for type-safe config with autocomplete:
111117

112118
```typescript
@@ -117,8 +123,8 @@ export default {
117123
format: 'ts',
118124
network: 'mainnet',
119125
contracts: [
120-
{ id: 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01' },
121-
{ id: 'SP2C2YFP12AJZB1KD5HQ4XFRYGEK02H70HVK8GQH.arkadiko-swap-v2-1' },
126+
{ id: 'SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01', name: 'amm-pool' },
127+
{ id: 'SP2C2YFP12AJZB1KD5HQ4XFRYGEK02H70HVK8GQH.arkadiko-swap-v2-1', name: 'arkadiko-swap' },
122128
],
123129
} satisfies AbiConfig;
124130
```
@@ -130,6 +136,22 @@ abi-cli sync
130136
# → reads abi.config.json (or .ts), writes all ABIs to outDir
131137
```
132138

139+
This generates:
140+
141+
```
142+
src/abis/
143+
├── amm-pool.ts
144+
├── arkadiko-swap.ts
145+
└── index.ts # barrel file with re-exports
146+
```
147+
148+
The barrel file re-exports all ABIs with camelCase names:
149+
150+
```typescript
151+
export { abi as ammPoolAbi } from './amm-pool.js';
152+
export { abi as arkadikoSwapAbi } from './arkadiko-swap.js';
153+
```
154+
133155
Use `--config` / `-c` to point to a custom config path:
134156

135157
```bash
@@ -145,8 +167,28 @@ abi-cli sync --config ./configs/my-abis.json
145167
| `network` | `string` | no | `"mainnet"` | Default network for all contracts |
146168
| `contracts` | `ContractEntry[]` | yes || List of contracts to sync |
147169
| `contracts[].id` | `string` | yes || Contract ID in `address.name` format |
170+
| `contracts[].name` | `string` | no | contract name from ID | Alias for output filename and barrel export |
148171
| `contracts[].network` | `string` | no | top-level `network` | Per-contract network override |
149172

173+
### Check — CI staleness detection
174+
175+
Use `--check` to verify local files are in sync with on-chain ABIs without writing anything. Exits with code 1 if any files are stale or missing.
176+
177+
```bash
178+
# Single contract
179+
abi-cli fetch SP102V8P0F7JX67ARQ77WEA3D3CFB5XW39REDT0AM.amm-pool-v2-01 --check
180+
181+
# All contracts from config
182+
abi-cli sync --check
183+
```
184+
185+
Add it to your CI pipeline to catch stale types:
186+
187+
```yaml
188+
- name: Check ABIs are up-to-date
189+
run: npx @satoshai/abi-cli sync --check
190+
```
191+
150192
## Flags Reference
151193
152194
### `abi-cli fetch`
@@ -157,13 +199,15 @@ abi-cli sync --config ./configs/my-abis.json
157199
| `--output` | `-o` | `<contract-name>.<format>` | Output file path (single contract only) |
158200
| `--format` | `-f` | `ts` | Output format: `ts` or `json` |
159201
| `--stdout` | | `false` | Print to stdout instead of writing a file |
202+
| `--check` | | `false` | Check if local files match on-chain ABI (exit 1 if stale) |
160203
| `--help` | | | Show help |
161204

162205
### `abi-cli sync`
163206

164207
| Flag | Alias | Default | Description |
165208
|------|-------|---------|-------------|
166209
| `--config` | `-c` | auto-discover | Path to config file |
210+
| `--check` | | `false` | Check if local files match on-chain ABIs (exit 1 if stale) |
167211
| `--help` | | | Show help |
168212

169213
## Programmatic API

src/config.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,7 @@ async function loadFile(filepath: string): Promise<AbiConfig> {
133133
}
134134

135135
function isFileNotFound(err: unknown): boolean {
136-
return (
137-
err instanceof Error &&
138-
'code' in err &&
139-
(err as NodeJS.ErrnoException).code === 'ENOENT'
140-
);
136+
if (!(err instanceof Error) || !('code' in err)) return false;
137+
const code = (err as NodeJS.ErrnoException).code;
138+
return code === 'ENOENT' || code === 'MODULE_NOT_FOUND';
141139
}

tests/unit/config.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,23 @@ describe('loadConfig', () => {
227227
vi.doUnmock('jiti');
228228
});
229229

230+
it('treats MODULE_NOT_FOUND from jiti as file not found', async () => {
231+
const notFound = Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
232+
vi.mocked(readFile).mockRejectedValue(notFound);
233+
234+
// jiti throws MODULE_NOT_FOUND instead of ENOENT
235+
const moduleNotFound = Object.assign(new Error('Cannot find module'), { code: 'MODULE_NOT_FOUND' });
236+
const mockImport = vi.fn().mockRejectedValue(moduleNotFound);
237+
const mockJiti = vi.fn().mockReturnValue({ import: mockImport });
238+
vi.doMock('jiti', () => ({ default: mockJiti }));
239+
240+
const { loadConfig: loadConfigFresh } = await import('../../src/config.js');
241+
242+
await expect(loadConfigFresh()).rejects.toThrow('No config file found');
243+
244+
vi.doUnmock('jiti');
245+
});
246+
230247
it('throws on invalid JSON content', async () => {
231248
vi.mocked(readFile).mockResolvedValueOnce('not json {{{');
232249

0 commit comments

Comments
 (0)