Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions MIGRATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,49 @@

This guide documents breaking changes to `@wraith-protocol/sdk` and how to update your code.

## Automated Migration with `@wraith-protocol/codemod`

Before working through the manual steps below, try the codemod -- it automates
the mechanical parts of each breaking change (import updates, message-matching
error handlers, the React Native polyfill call) so you don't have to
grep-and-sed by hand.

```bash
npx @wraith-protocol/codemod v1 ./src
```

Run it with `--dry --print` first if you want to preview the diff without
writing anything:

```bash
npx @wraith-protocol/codemod v1 ./src --dry --print
```

It's safe to run more than once -- files that are already migrated, or that
don't match a known pattern, are left untouched.

**What it handles automatically:**

- Rewrites `catch (e) { if (e.message.includes('...')) }` message-matching
into `e instanceof <TypedError>` checks, and adds the required import (see
[Error Handling](#error-handling-from-message-matching-to-typed-exceptions-150)
below).
- Inserts the `installReactNativePolyfills()` call and import into React
Native entry files that need it (see
[React Native](#react-native-explicit-polyfill-installation-required-150)
below).

**What still needs a manual look:** the codemod only rewrites `.message.includes(...)`
checks against message fragments it recognizes as belonging to a specific
`@wraith-protocol/sdk` error class. If your code matches against custom or
already-changed message text, or combines multiple `.message.includes(...)`
checks with `||`/`&&` in a single condition, review those call sites by hand
using the reference below. The Stellar cryptographic audit fixes require no
code changes at all (automated or manual) -- see that section for details.

Source lives in [`packages/codemod`](./packages/codemod), including the fixture
pre/post pairs each transform is tested against.

## Upgrading to 2.0.0

### Error Handling: From Message Matching to Typed Exceptions (1.5.0+)
Expand Down
77 changes: 77 additions & 0 deletions packages/codemod/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# @wraith-protocol/codemod

Codemods that automate the mechanical parts of migrating an app across
`@wraith-protocol/sdk` major versions -- so upgrading doesn't mean
grep-and-sed by hand.

## Usage

```bash
npx @wraith-protocol/codemod <version> [path] [options]
```

- `version` -- which transform set to run (currently `v1`), matching a folder
under [`transforms/`](./transforms).
- `path` -- file or directory to transform. Defaults to the current directory.

```bash
# Preview the diff without writing anything
npx @wraith-protocol/codemod v1 ./src --dry --print

# Apply it
npx @wraith-protocol/codemod v1 ./src
```

It's safe to run more than once: every transform in this package is
idempotent, and files that don't match a known pattern are left untouched.

### Options

| Flag | Description |
| -------------- | ----------------------------------------------------------------------------- |
| `--dry` | Run without writing any changes to disk. |
| `--print` | Print transformed output to stdout. |
| `--extensions` | Comma-separated file extensions to process. Defaults to `ts,tsx,js,jsx`. |
| `--ignore` | Glob to skip. Can be passed more than once. `node_modules` is always ignored. |

## What `v1` covers

Each transform in `transforms/v1/` corresponds to one breaking change
documented in [`MIGRATING.md`](../../MIGRATING.md):

- **`typed-error-catch.cjs`** -- rewrites `catch (e) { if (e.message.includes('...')) }`
message-matching into `e instanceof <TypedError>` checks, against a table of
known, stable message fragments sourced directly from `src/errors.ts`. It
also adds/merges the required named import from `@wraith-protocol/sdk`.
Only recognized fragments are rewritten -- anything else is left alone.

- **`install-react-native-polyfills.cjs`** -- detects React Native entry
files (files importing from both `react-native` and `@wraith-protocol/sdk`)
and inserts the now-required `installReactNativePolyfills()` call and
import, if one isn't already present.

Every transform has a fixture pair under [`fixtures/`](./fixtures) (an
`input.*` / `output.*` file), plus a `no-op-file` fixture used to confirm each
transform leaves non-matching code untouched. See [`test/`](./test) for the
snapshot-style tests that run each transform against its fixtures, plus an
end-to-end test that runs the same jscodeshift `Runner` the CLI uses against
a temp fixture app and checks idempotency across two full passes.

## Programmatic API

```ts
import { runCodemod, listTransformSets, listTransforms } from '@wraith-protocol/codemod';

const results = await runCodemod({ version: 'v1', target: './src' });
```

## Adding a transform for a future major version

1. Create `transforms/v<N>/your-transform.cjs`, exporting a standard
jscodeshift transform function (`module.exports = function (fileInfo, api, options) { ... }`).
2. Add an `input.*` / `output.*` fixture pair under
`fixtures/your-transform-name/`.
3. Add a test in `test/` asserting the transform matches the fixture output
and is idempotent when run against its own output a second time.
4. Document the change in the root `MIGRATING.md`, and link to it from the
"Automated Migration" section at the top.
136 changes: 136 additions & 0 deletions packages/codemod/bin/cli.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/env node
import { fileURLToPath } from 'node:url';
import path from 'node:path';
import fs from 'node:fs';
import { run as runJscodeshift } from 'jscodeshift/src/Runner.js';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const packageRoot = path.resolve(__dirname, '..');

function printUsage() {
console.log(`
@wraith-protocol/codemod

Usage:
npx @wraith-protocol/codemod <version> [path] [options]

Arguments:
version Transform set to run (e.g. "v1"). Matches a folder under
transforms/.
path File or directory to transform. Defaults to the current
directory.

Options:
--dry Run without writing any changes to disk.
--print Print transformed output to stdout (implies --dry unless
combined with a write-enabled run).
--extensions Comma-separated list of file extensions to process.
Defaults to "ts,tsx,js,jsx".
--ignore Glob pattern of files/directories to skip. Can be passed
more than once. node_modules is always ignored.
-h, --help Show this help message.

Examples:
npx @wraith-protocol/codemod v1 ./src
npx @wraith-protocol/codemod v1 ./src --dry --print
`);
}

function parseArgs(argv) {
const args = { flags: {}, positionals: [], ignore: [] };
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === '-h' || arg === '--help') {
args.flags.help = true;
} else if (arg === '--dry') {
args.flags.dry = true;
} else if (arg === '--print') {
args.flags.print = true;
} else if (arg === '--extensions') {
args.flags.extensions = argv[++i];
} else if (arg === '--ignore') {
args.ignore.push(argv[++i]);
} else if (arg.startsWith('-')) {
console.error(`Unknown option: ${arg}`);
process.exit(1);
} else {
args.positionals.push(arg);
}
}
return args;
}

async function main() {
const args = parseArgs(process.argv.slice(2));

if (args.flags.help || args.positionals.length === 0) {
printUsage();
process.exit(args.flags.help ? 0 : 1);
}

const [version, targetArg] = args.positionals;
const transformsDir = path.join(packageRoot, 'transforms', version);

if (!fs.existsSync(transformsDir)) {
console.error(`Unknown transform set "${version}" (no directory at ${transformsDir}).`);
const available = fs
.readdirSync(path.join(packageRoot, 'transforms'), { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name);
console.error(`Available transform sets: ${available.join(', ') || '(none)'}`);
process.exit(1);
}

const transformFiles = fs
.readdirSync(transformsDir)
.filter((file) => file.endsWith('.cjs') || file.endsWith('.js'))
.sort()
.map((file) => path.join(transformsDir, file));

if (transformFiles.length === 0) {
console.error(`No transforms found in ${transformsDir}.`);
process.exit(1);
}

const target = targetArg ? path.resolve(process.cwd(), targetArg) : process.cwd();
if (!fs.existsSync(target)) {
console.error(`Target path does not exist: ${target}`);
process.exit(1);
}

const jscodeshiftOptions = {
dry: Boolean(args.flags.dry),
print: Boolean(args.flags.print),
verbose: 0,
babel: true,
extensions: args.flags.extensions || 'ts,tsx,js,jsx',
parser: 'tsx',
ignorePattern: ['**/node_modules/**', ...args.ignore],
silent: false,
runInBand: false,
};

console.log(`@wraith-protocol/codemod: running "${version}" transforms against ${target}\n`);

let anyErrors = false;

for (const transformFile of transformFiles) {
const name = path.basename(transformFile);
console.log(`--- ${name} ---`);
const result = await runJscodeshift(transformFile, [target], jscodeshiftOptions);
if (result.error > 0) {
anyErrors = true;
}
console.log('');
}

if (anyErrors) {
console.error('One or more transforms reported errors. See output above.');
process.exit(1);
}
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { AppRegistry } from 'react-native';
import { scanAnnouncements } from '@wraith-protocol/sdk/chains/stellar';
import App from './App';

AppRegistry.registerComponent('MyApp', () => App);
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { AppRegistry } from 'react-native';
import { scanAnnouncements } from '@wraith-protocol/sdk/chains/stellar';
import App from './App';

import { installReactNativePolyfills } from '@wraith-protocol/sdk';
installReactNativePolyfills();

AppRegistry.registerComponent('MyApp', () => App);
11 changes: 11 additions & 0 deletions packages/codemod/fixtures/no-op-file/input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { deriveStealthKeys } from '@wraith-protocol/sdk/chains/stellar';

export function tryDerive(signature: string) {
try {
return deriveStealthKeys(signature);
} catch (e) {
// Generic catch, not matching any known @wraith-protocol/sdk error
// message fragment -- should be left completely untouched.
console.log('unexpected error', e.message.includes('some unrelated string'));
}
}
15 changes: 15 additions & 0 deletions packages/codemod/fixtures/typed-error-catch/input.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { deriveStealthKeys } from '@wraith-protocol/sdk/chains/stellar';

export function tryDerive(signature: string) {
try {
return deriveStealthKeys(signature);
} catch (e) {
if (e.message.includes('Invalid signature length or format')) {
console.log('bad signature');
} else if (e.message.includes('Key derivation failed')) {
console.log('derivation failed');
} else {
throw e;
}
}
}
17 changes: 17 additions & 0 deletions packages/codemod/fixtures/typed-error-catch/output.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { deriveStealthKeys } from '@wraith-protocol/sdk/chains/stellar';

import { InvalidSignatureError, KeyDerivationFailedError } from '@wraith-protocol/sdk';

export function tryDerive(signature: string) {
try {
return deriveStealthKeys(signature);
} catch (e) {
if (e instanceof InvalidSignatureError) {
console.log('bad signature');
} else if (e instanceof KeyDerivationFailedError) {
console.log('derivation failed');
} else {
throw e;
}
}
}
34 changes: 34 additions & 0 deletions packages/codemod/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "@wraith-protocol/codemod",
"version": "0.1.0",
"private": false,
"type": "module",
"description": "Codemods that automate mechanical migrations across @wraith-protocol/sdk major versions.",
"bin": {
"wraith-codemod": "./bin/cli.mjs"
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist",
"bin",
"transforms"
],
"scripts": {
"build": "tsup",
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit -p tsconfig.json",
"clean": "rm -rf dist"
},
"dependencies": {
"jscodeshift": "^17.4.0"
},
"devDependencies": {
"@types/jscodeshift": "^0.12.0",
"@types/node": "^20.19.43",
"tsup": "^8.4.0",
"typescript": "^5.7.0",
"vitest": "^3.1.0"
}
}
Loading
Loading