From f6692929cc6f34402c9333616d84927e73a36a71 Mon Sep 17 00:00:00 2001 From: Jerryvic911 Date: Fri, 28 Aug 2026 05:47:00 +0100 Subject: [PATCH 1/2] Add @wraith-protocol/codemod package for v1 migrations (#178) --- MIGRATING.md | 43 +++++ packages/codemod/README.md | 77 +++++++++ packages/codemod/bin/cli.mjs | 136 +++++++++++++++ .../install-react-native-polyfills/input.tsx | 5 + .../install-react-native-polyfills/output.tsx | 8 + packages/codemod/fixtures/no-op-file/input.ts | 11 ++ .../fixtures/typed-error-catch/input.ts | 15 ++ .../fixtures/typed-error-catch/output.ts | 17 ++ packages/codemod/package.json | 34 ++++ packages/codemod/src/index.ts | 86 +++++++++ packages/codemod/test/cli.test.ts | 94 ++++++++++ packages/codemod/test/helpers.ts | 51 ++++++ .../install-react-native-polyfills.test.ts | 67 +++++++ .../codemod/test/typed-error-catch.test.ts | 31 ++++ .../v1/install-react-native-polyfills.cjs | 105 +++++++++++ .../transforms/v1/typed-error-catch.cjs | 163 ++++++++++++++++++ packages/codemod/tsconfig.json | 17 ++ packages/codemod/tsup.config.ts | 13 ++ packages/codemod/vitest.config.ts | 8 + pnpm-lock.yaml | 124 +++++++++++++ 20 files changed, 1105 insertions(+) create mode 100644 packages/codemod/README.md create mode 100644 packages/codemod/bin/cli.mjs create mode 100644 packages/codemod/fixtures/install-react-native-polyfills/input.tsx create mode 100644 packages/codemod/fixtures/install-react-native-polyfills/output.tsx create mode 100644 packages/codemod/fixtures/no-op-file/input.ts create mode 100644 packages/codemod/fixtures/typed-error-catch/input.ts create mode 100644 packages/codemod/fixtures/typed-error-catch/output.ts create mode 100644 packages/codemod/package.json create mode 100644 packages/codemod/src/index.ts create mode 100644 packages/codemod/test/cli.test.ts create mode 100644 packages/codemod/test/helpers.ts create mode 100644 packages/codemod/test/install-react-native-polyfills.test.ts create mode 100644 packages/codemod/test/typed-error-catch.test.ts create mode 100644 packages/codemod/transforms/v1/install-react-native-polyfills.cjs create mode 100644 packages/codemod/transforms/v1/typed-error-catch.cjs create mode 100644 packages/codemod/tsconfig.json create mode 100644 packages/codemod/tsup.config.ts create mode 100644 packages/codemod/vitest.config.ts diff --git a/MIGRATING.md b/MIGRATING.md index fbe7956..d8b66a6 100644 --- a/MIGRATING.md +++ b/MIGRATING.md @@ -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 ` 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+) diff --git a/packages/codemod/README.md b/packages/codemod/README.md new file mode 100644 index 0000000..b136d6b --- /dev/null +++ b/packages/codemod/README.md @@ -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 [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 ` 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/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. diff --git a/packages/codemod/bin/cli.mjs b/packages/codemod/bin/cli.mjs new file mode 100644 index 0000000..af29e9c --- /dev/null +++ b/packages/codemod/bin/cli.mjs @@ -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 [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); +}); diff --git a/packages/codemod/fixtures/install-react-native-polyfills/input.tsx b/packages/codemod/fixtures/install-react-native-polyfills/input.tsx new file mode 100644 index 0000000..b5da105 --- /dev/null +++ b/packages/codemod/fixtures/install-react-native-polyfills/input.tsx @@ -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); diff --git a/packages/codemod/fixtures/install-react-native-polyfills/output.tsx b/packages/codemod/fixtures/install-react-native-polyfills/output.tsx new file mode 100644 index 0000000..35044e3 --- /dev/null +++ b/packages/codemod/fixtures/install-react-native-polyfills/output.tsx @@ -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); diff --git a/packages/codemod/fixtures/no-op-file/input.ts b/packages/codemod/fixtures/no-op-file/input.ts new file mode 100644 index 0000000..b277e5f --- /dev/null +++ b/packages/codemod/fixtures/no-op-file/input.ts @@ -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')); + } +} diff --git a/packages/codemod/fixtures/typed-error-catch/input.ts b/packages/codemod/fixtures/typed-error-catch/input.ts new file mode 100644 index 0000000..3f57685 --- /dev/null +++ b/packages/codemod/fixtures/typed-error-catch/input.ts @@ -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; + } + } +} diff --git a/packages/codemod/fixtures/typed-error-catch/output.ts b/packages/codemod/fixtures/typed-error-catch/output.ts new file mode 100644 index 0000000..ec4fa95 --- /dev/null +++ b/packages/codemod/fixtures/typed-error-catch/output.ts @@ -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; + } + } +} diff --git a/packages/codemod/package.json b/packages/codemod/package.json new file mode 100644 index 0000000..054be1a --- /dev/null +++ b/packages/codemod/package.json @@ -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" + } +} diff --git a/packages/codemod/src/index.ts b/packages/codemod/src/index.ts new file mode 100644 index 0000000..e010f12 --- /dev/null +++ b/packages/codemod/src/index.ts @@ -0,0 +1,86 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore -- jscodeshift's Runner has no published types for this entry point +import { run as runJscodeshift } from 'jscodeshift/src/Runner.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const packageRoot = path.resolve(__dirname, '..'); + +export interface RunCodemodOptions { + /** Transform set to run, e.g. "v1". Matches a folder under transforms/. */ + version: string; + /** File or directory to transform. */ + target: string; + /** Run without writing changes to disk. */ + dry?: boolean; + /** Print transformed output to stdout. */ + print?: boolean; + /** Comma-separated list of extensions. Defaults to "ts,tsx,js,jsx". */ + extensions?: string; +} + +export interface RunCodemodResult { + transform: string; + ok: number; + nochange: number; + skip: number; + error: number; +} + +/** List the transform-set names (e.g. ["v1"]) available in this package. */ +export function listTransformSets(): string[] { + const transformsRoot = path.join(packageRoot, 'transforms'); + return fs + .readdirSync(transformsRoot, { withFileTypes: true }) + .filter((entry) => entry.isDirectory()) + .map((entry) => entry.name) + .sort(); +} + +/** List the individual transform file names within a transform set. */ +export function listTransforms(version: string): string[] { + const dir = path.join(packageRoot, 'transforms', version); + if (!fs.existsSync(dir)) { + throw new Error(`Unknown transform set "${version}" (no directory at ${dir}).`); + } + return fs + .readdirSync(dir) + .filter((file) => file.endsWith('.cjs') || file.endsWith('.js')) + .sort(); +} + +/** + * Run every transform in the given transform set against the target path, + * in file-name order. Mirrors the CLI's behavior, for callers that want to + * invoke the codemod programmatically instead of shelling out. + */ +export async function runCodemod(options: RunCodemodOptions): Promise { + const dir = path.join(packageRoot, 'transforms', options.version); + if (!fs.existsSync(dir)) { + throw new Error(`Unknown transform set "${options.version}" (no directory at ${dir}).`); + } + + const transformFiles = listTransforms(options.version).map((file) => path.join(dir, file)); + const target = path.resolve(options.target); + + const results: RunCodemodResult[] = []; + + for (const transformFile of transformFiles) { + const result = await runJscodeshift(transformFile, [target], { + dry: Boolean(options.dry), + print: Boolean(options.print), + verbose: 0, + babel: true, + extensions: options.extensions || 'ts,tsx,js,jsx', + parser: 'tsx', + ignorePattern: ['**/node_modules/**'], + silent: true, + runInBand: false, + }); + results.push({ transform: path.basename(transformFile), ...result }); + } + + return results; +} diff --git a/packages/codemod/test/cli.test.ts b/packages/codemod/test/cli.test.ts new file mode 100644 index 0000000..7547f2c --- /dev/null +++ b/packages/codemod/test/cli.test.ts @@ -0,0 +1,94 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { afterEach, beforeEach, describe, expect, test } from 'vitest'; +// @ts-expect-error -- no published types for this entry point +import { run as runJscodeshift } from 'jscodeshift/src/Runner.js'; +import { loadFixture, normalize, packageRoot } from './helpers.js'; + +let tempDir: string; + +beforeEach(() => { + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'wraith-codemod-')); + const appDir = path.join(tempDir, 'app'); + fs.mkdirSync(appDir, { recursive: true }); + + fs.writeFileSync( + path.join(appDir, 'errors.ts'), + loadFixture('typed-error-catch', 'input.ts'), + ); + fs.writeFileSync( + path.join(appDir, 'index.tsx'), + loadFixture('install-react-native-polyfills', 'input.tsx'), + ); +}); + +afterEach(() => { + fs.rmSync(tempDir, { recursive: true, force: true }); +}); + +const runnerOptions = { + babel: true, + extensions: 'ts,tsx,js,jsx', + parser: 'tsx', + silent: true, + runInBand: true, +}; + +async function runAllV1Transforms(target: string) { + const transformsDir = path.join(packageRoot, 'transforms', 'v1'); + const transformFiles = fs + .readdirSync(transformsDir) + .filter((f) => f.endsWith('.cjs')) + .sort() + .map((f) => path.join(transformsDir, f)); + + const results = []; + for (const transformFile of transformFiles) { + results.push(await runJscodeshift(transformFile, [target], runnerOptions)); + } + return results; +} + +describe('CLI end-to-end (via jscodeshift Runner, as bin/cli.mjs uses it)', () => { + test('produces the expected diff against a fixture app', async () => { + const appDir = path.join(tempDir, 'app'); + + const results = await runAllV1Transforms(appDir); + + // Each transform should have modified exactly one of the two files. + const totalOk = results.reduce((sum, r) => sum + r.ok, 0); + expect(totalOk).toBe(2); + const totalErrors = results.reduce((sum, r) => sum + r.error, 0); + expect(totalErrors).toBe(0); + + const errorsOut = fs.readFileSync(path.join(appDir, 'errors.ts'), 'utf8'); + const indexOut = fs.readFileSync(path.join(appDir, 'index.tsx'), 'utf8'); + + expect(normalize(errorsOut)).toBe(normalize(loadFixture('typed-error-catch', 'output.ts'))); + expect(normalize(indexOut)).toBe( + normalize(loadFixture('install-react-native-polyfills', 'output.tsx')), + ); + }, 30000); + + test('produces the expected diff against a fixture app', async () => { + const appDir = path.join(tempDir, 'app'); + + await runAllV1Transforms(appDir); + const afterFirstRun = { + errors: fs.readFileSync(path.join(appDir, 'errors.ts'), 'utf8'), + index: fs.readFileSync(path.join(appDir, 'index.tsx'), 'utf8'), + }; + + const secondRunResults = await runAllV1Transforms(appDir); + + const afterSecondRun = { + errors: fs.readFileSync(path.join(appDir, 'errors.ts'), 'utf8'), + index: fs.readFileSync(path.join(appDir, 'index.tsx'), 'utf8'), + }; + + expect(afterSecondRun).toEqual(afterFirstRun); + const totalOkOnSecondPass = secondRunResults.reduce((sum, r) => sum + r.ok, 0); + expect(totalOkOnSecondPass).toBe(0); + }, 30000); +}); diff --git a/packages/codemod/test/helpers.ts b/packages/codemod/test/helpers.ts new file mode 100644 index 0000000..b6d4536 --- /dev/null +++ b/packages/codemod/test/helpers.ts @@ -0,0 +1,51 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { createRequire } from 'node:module'; +import { fileURLToPath } from 'node:url'; +import jscodeshiftCore from 'jscodeshift/src/core.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +export const packageRoot = path.resolve(__dirname, '..'); +const require = createRequire(import.meta.url); + +const j = jscodeshiftCore.withParser('tsx'); + +export function loadFixture(name: string, file: string): string { + const fixturePath = path.join(packageRoot, 'fixtures', name, file); + return fs.readFileSync(fixturePath, 'utf8'); +} + +export function loadTransform(version: string, transformName: string) { + const transformPath = path.join(packageRoot, 'transforms', version, transformName); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const mod = require(transformPath); + return mod.default || mod; +} + +export function applyTransform( + transform: (fileInfo: unknown, api: unknown, options: unknown) => string, + source: string, + filePath = 'test.tsx', +): string { + const fileInfo = { source, path: filePath }; + const api = { + jscodeshift: j, + j, + stats: () => undefined, + report: () => undefined, + }; + return transform(fileInfo, api, { printOptions: { quote: 'single' } }); +} +/** + * Normalizes recast/jscodeshift output for comparison across environments: + * strips trailing whitespace per line and collapses blank lines. Different + * recast versions format blank lines between inserted nodes slightly + * differently -- this keeps tests focused on actual content, not that noise. + */ +export function normalize(source: string): string { + return source + .replace(/\r\n/g, '\n') + .replace(/[ \t]+$/gm, '') + .replace(/\n{2,}/g, '\n') + .trim(); +} \ No newline at end of file diff --git a/packages/codemod/test/install-react-native-polyfills.test.ts b/packages/codemod/test/install-react-native-polyfills.test.ts new file mode 100644 index 0000000..5ed4f73 --- /dev/null +++ b/packages/codemod/test/install-react-native-polyfills.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, test } from 'vitest'; +import { applyTransform, loadFixture, loadTransform, normalize } from './helpers.js'; + +const transform = loadTransform('v1', 'install-react-native-polyfills.cjs'); + +describe('install-react-native-polyfills', () => { + test('inserts the import and startup call in a React Native entry file', () => { + const input = loadFixture('install-react-native-polyfills', 'input.tsx'); + const expected = loadFixture('install-react-native-polyfills', 'output.tsx'); + + const actual = applyTransform(transform, input); + + expect(normalize(actual)).toBe(normalize(expected)); + }); + + test('is idempotent: running the already-migrated output again is a no-op', () => { + const alreadyMigrated = loadFixture('install-react-native-polyfills', 'output.tsx'); + + const actual = applyTransform(transform, alreadyMigrated); + + expect(normalize(actual)).toBe(normalize(alreadyMigrated)); + }); + + test('merges into an existing @wraith-protocol/sdk import instead of duplicating it', () => { + const input = `import { AppRegistry } from 'react-native'; +import { ScannerPool } from '@wraith-protocol/sdk'; +import App from './App'; + +AppRegistry.registerComponent('MyApp', () => App); +`; + + const actual = applyTransform(transform, input); + + expect(actual).toContain( + "import { installReactNativePolyfills, ScannerPool } from '@wraith-protocol/sdk';", + ); + expect(actual).toContain('installReactNativePolyfills();'); + // Only one import statement from the SDK root -- not duplicated. + expect(actual.match(/from '@wraith-protocol\/sdk';/g)).toHaveLength(1); + }); + + test('leaves non-React-Native files untouched', () => { + const input = `import { scanAnnouncements } from '@wraith-protocol/sdk/chains/stellar'; + +export function scan() { + return scanAnnouncements([], new Uint8Array(), new Uint8Array(), 0n); +} +`; + + const actual = applyTransform(transform, input); + + expect(actual).toBe(input); + }); + + test('leaves React-Native files that do not import the SDK untouched', () => { + const input = `import { View } from 'react-native'; + +export function Empty() { + return null; +} +`; + + const actual = applyTransform(transform, input); + + expect(actual).toBe(input); + }); +}); diff --git a/packages/codemod/test/typed-error-catch.test.ts b/packages/codemod/test/typed-error-catch.test.ts new file mode 100644 index 0000000..c1c8412 --- /dev/null +++ b/packages/codemod/test/typed-error-catch.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, test } from 'vitest'; +import { applyTransform, loadFixture, loadTransform, normalize } from './helpers.js'; + +const transform = loadTransform('v1', 'typed-error-catch.cjs'); + +describe('typed-error-catch', () => { + test('rewrites .message.includes() checks to instanceof and imports the error classes', () => { + const input = loadFixture('typed-error-catch', 'input.ts'); + const expected = loadFixture('typed-error-catch', 'output.ts'); + + const actual = applyTransform(transform, input); + + expect(normalize(actual)).toBe(normalize(expected)); + }); + + test('is idempotent: running the already-migrated output again is a no-op', () => { + const alreadyMigrated = loadFixture('typed-error-catch', 'output.ts'); + + const actual = applyTransform(transform, alreadyMigrated); + + expect(normalize(actual)).toBe(normalize(alreadyMigrated)); + }); + + test('leaves files with unrecognized message substrings untouched', () => { + const input = loadFixture('no-op-file', 'input.ts'); + + const actual = applyTransform(transform, input); + + expect(actual).toBe(input); + }); +}); diff --git a/packages/codemod/transforms/v1/install-react-native-polyfills.cjs b/packages/codemod/transforms/v1/install-react-native-polyfills.cjs new file mode 100644 index 0000000..79f2217 --- /dev/null +++ b/packages/codemod/transforms/v1/install-react-native-polyfills.cjs @@ -0,0 +1,105 @@ +/** + * Transform: install-react-native-polyfills + * + * Migrates the React Native breaking change documented in MIGRATING.md + * (1.5.0+): polyfills are no longer auto-installed, so React Native apps + * must call `installReactNativePolyfills()` once at startup, before any + * @wraith-protocol/sdk crypto imports are used. + * + * Heuristic: a file is treated as a React Native entry point if it imports + * from 'react-native' AND imports from '@wraith-protocol/sdk' (root or a + * chain subpath). If so, this transform: + * + * 1. Ensures `installReactNativePolyfills` is imported from + * '@wraith-protocol/sdk' (merged into an existing root import if one + * exists). + * 2. Inserts `installReactNativePolyfills();` as the first statement + * after the file's import block, if no call to it already exists + * anywhere in the file. + * + * Idempotent: if a call to installReactNativePolyfills() is already present + * anywhere in the file, the transform is a no-op on subsequent runs. + */ + +'use strict'; + +const SDK_ROOT_SOURCE = '@wraith-protocol/sdk'; +const POLYFILL_FN = 'installReactNativePolyfills'; + +module.exports = function transform(fileInfo, api, options) { + const j = api.jscodeshift; + const root = j(fileInfo.source); + const printOptions = options.printOptions || { quote: 'single' }; + + const importsReactNative = + root.find(j.ImportDeclaration).filter((p) => p.node.source.value === 'react-native').size() > + 0; + + const importsSdk = + root + .find(j.ImportDeclaration) + .filter( + (p) => + typeof p.node.source.value === 'string' && + p.node.source.value.startsWith(SDK_ROOT_SOURCE), + ) + .size() > 0; + + if (!importsReactNative || !importsSdk) { + return fileInfo.source; + } + + const alreadyCalled = + root + .find(j.CallExpression, { + callee: { type: 'Identifier', name: POLYFILL_FN }, + }) + .size() > 0; + + if (alreadyCalled) { + return fileInfo.source; + } + + ensureNamedImport(j, root, SDK_ROOT_SOURCE, POLYFILL_FN); + insertCallAfterImports(j, root, POLYFILL_FN); + + return root.toSource(printOptions); +}; + +function ensureNamedImport(j, root, source, name) { + const existing = root + .find(j.ImportDeclaration) + .filter((p) => p.node.source.value === source); + + if (existing.size() > 0) { + const decl = existing.paths()[0].node; + const hasIt = decl.specifiers.some( + (s) => s.type === 'ImportSpecifier' && s.imported.name === name, + ); + if (!hasIt) { + decl.specifiers.unshift(j.importSpecifier(j.identifier(name))); + } + return; + } + + const newImport = j.importDeclaration( + [j.importSpecifier(j.identifier(name))], + j.literal(source), + ); + const body = root.get().node.program.body; + const lastImportIndex = body.reduce( + (acc, node, index) => (node.type === 'ImportDeclaration' ? index : acc), + -1, + ); + body.splice(lastImportIndex + 1, 0, newImport); +} + +function insertCallAfterImports(j, root, name) { + const body = root.get().node.program.body; + const lastImportIndex = body.reduce( + (acc, node, index) => (node.type === 'ImportDeclaration' ? index : acc), + -1, + ); + const callStatement = j.expressionStatement(j.callExpression(j.identifier(name), [])); + body.splice(lastImportIndex + 1, 0, callStatement); +} diff --git a/packages/codemod/transforms/v1/typed-error-catch.cjs b/packages/codemod/transforms/v1/typed-error-catch.cjs new file mode 100644 index 0000000..bbb4b4d --- /dev/null +++ b/packages/codemod/transforms/v1/typed-error-catch.cjs @@ -0,0 +1,163 @@ +/** + * Transform: typed-error-catch + * + * Migrates the "message-matching" error handling pattern documented in + * MIGRATING.md ยง Error Handling (1.5.0+) to the typed-exception pattern: + * + * catch (e) { + * if (e.message.includes('Invalid name:')) { ... } + * } + * + * -> + * + * catch (e) { + * if (e instanceof InvalidNameError) { ... } + * } + * + * and adds/merges the required named import from '@wraith-protocol/sdk'. + * + * Only literal substrings that are known, stable fragments of an actual + * @wraith-protocol/sdk error message are rewritten. Anything else is left + * untouched -- this transform never guesses. + * + * Idempotent: after transformation, no `.message.includes(...)` call sites + * matching the table below remain, so a second run is a structural no-op. + */ + +'use strict'; + +// Canonical, stable message fragments -> the error class that throws them. +// Sourced directly from src/errors.ts constructors (the literal text before +// any runtime-interpolated values). +const MESSAGE_TO_ERROR_CLASS = { + 'Invalid stealth meta-address format': 'InvalidMetaAddressError', + 'Invalid name:': 'InvalidNameError', + 'Invalid signature length or format': 'InvalidSignatureError', + 'Invalid cryptographic scalar': 'InvalidScalarError', + 'Key derivation failed': 'KeyDerivationFailedError', + 'View tag mismatch': 'ViewTagMismatchError', + 'ECDH operation failed': 'ECDHFailedError', + 'Elliptic Curve Diffie-Hellman (ECDH) operation failed': 'ECDHFailedError', + 'RPC request failed': 'RPCRequestError', + 'RPC request retries exhausted': 'RPCRetryExhaustedError', + 'Retention limit exceeded': 'RetentionExceededError', + 'Name not found:': 'NameNotFoundError', + 'Name is already registered': 'NameAlreadyRegisteredError', + 'Insufficient authority to perform operation': 'InsufficientAuthError', + 'Smart contract transaction reverted': 'ContractRevertError', + 'Insufficient balance to build transaction': 'InsufficientBalanceError', +}; + +const SDK_SOURCE = '@wraith-protocol/sdk'; + +/** + * Resolve a literal string a developer might have matched against to the + * error class it corresponds to. Matches exact fragments, or a literal + * that contains / is contained by a known canonical fragment. + */ +function resolveErrorClass(literal) { + if (Object.prototype.hasOwnProperty.call(MESSAGE_TO_ERROR_CLASS, literal)) { + return MESSAGE_TO_ERROR_CLASS[literal]; + } + for (const [fragment, className] of Object.entries(MESSAGE_TO_ERROR_CLASS)) { + if (literal.includes(fragment) || fragment.includes(literal)) { + return className; + } + } + return null; +} + +module.exports = function transform(fileInfo, api, options) { + const j = api.jscodeshift; + const root = j(fileInfo.source); + const printOptions = options.printOptions || { quote: 'single' }; + + let mutated = false; + const requiredClasses = new Set(); + + root.find(j.CatchClause).forEach((catchPath) => { + const param = catchPath.node.param; + if (!param || param.type !== 'Identifier') return; + const errName = param.name; + + j(catchPath) + .find(j.IfStatement) + .forEach((ifPath) => { + const test = ifPath.node.test; + if (!isMessageIncludesCall(test, errName)) return; + + const literalArg = test.arguments[0]; + if (!literalArg || literalArg.type !== 'StringLiteral') return; + + const errorClass = resolveErrorClass(literalArg.value); + if (!errorClass) return; + + ifPath.node.test = j.binaryExpression( + 'instanceof', + j.identifier(errName), + j.identifier(errorClass), + ); + requiredClasses.add(errorClass); + mutated = true; + }); + }); + + if (!mutated) { + return fileInfo.source; + } + + ensureNamedImport(j, root, SDK_SOURCE, requiredClasses); + + return root.toSource(printOptions); +}; + +function isMessageIncludesCall(node, errName) { + return ( + node && + node.type === 'CallExpression' && + node.arguments.length === 1 && + node.callee.type === 'MemberExpression' && + !node.callee.computed && + node.callee.property.type === 'Identifier' && + node.callee.property.name === 'includes' && + node.callee.object.type === 'MemberExpression' && + !node.callee.object.computed && + node.callee.object.property.type === 'Identifier' && + node.callee.object.property.name === 'message' && + node.callee.object.object.type === 'Identifier' && + node.callee.object.object.name === errName + ); +} + +function ensureNamedImport(j, root, source, names) { + if (names.size === 0) return; + + const existing = root + .find(j.ImportDeclaration) + .filter((p) => p.node.source.value === source); + + if (existing.size() > 0) { + const decl = existing.paths()[0].node; + const existingNames = new Set( + decl.specifiers + .filter((s) => s.type === 'ImportSpecifier') + .map((s) => s.imported.name), + ); + for (const name of names) { + if (!existingNames.has(name)) { + decl.specifiers.push(j.importSpecifier(j.identifier(name))); + } + } + return; + } + + const specifiers = [...names].map((name) => j.importSpecifier(j.identifier(name))); + const newImport = j.importDeclaration(specifiers, j.literal(source)); + + const body = root.get().node.program.body; + const lastImportIndex = body.reduce( + (acc, node, index) => (node.type === 'ImportDeclaration' ? index : acc), + -1, + ); + body.splice(lastImportIndex + 1, 0, newImport); +} diff --git a/packages/codemod/tsconfig.json b/packages/codemod/tsconfig.json new file mode 100644 index 0000000..6fc3d67 --- /dev/null +++ b/packages/codemod/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "declaration": true, + "types": ["node"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "outDir": "dist", + "rootDir": "." + }, + "include": ["src", "transforms"], + "exclude": ["node_modules", "dist", "test", "fixtures"] +} diff --git a/packages/codemod/tsup.config.ts b/packages/codemod/tsup.config.ts new file mode 100644 index 0000000..904c0d2 --- /dev/null +++ b/packages/codemod/tsup.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: { + index: 'src/index.ts', + }, + format: ['esm'], + dts: true, + splitting: false, + clean: true, + treeshake: true, + external: ['jscodeshift'], +}); diff --git a/packages/codemod/vitest.config.ts b/packages/codemod/vitest.config.ts new file mode 100644 index 0000000..a71174d --- /dev/null +++ b/packages/codemod/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['test/**/*.test.ts'], + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6d41f3d..32c3b1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -432,6 +432,28 @@ importers: specifier: ^2.0.0 version: 2.2.12(typescript@5.9.3) + packages/codemod: + dependencies: + jscodeshift: + specifier: ^17.4.0 + version: 17.4.0(@babel/preset-env@7.29.7(@babel/core@7.29.7)) + devDependencies: + '@types/jscodeshift': + specifier: ^0.12.0 + version: 0.12.0 + '@types/node': + specifier: ^20.19.43 + version: 20.19.43 + tsup: + specifier: ^8.4.0 + version: 8.5.1(@microsoft/api-extractor@7.58.12(@types/node@20.19.43))(jiti@2.6.1)(postcss@8.5.16)(tsx@4.23.0)(typescript@5.9.3)(yaml@2.9.0) + typescript: + specifier: ^5.7.0 + version: 5.9.3 + vitest: + specifier: ^3.1.0 + version: 3.2.4(@types/node@20.19.43)(jiti@2.6.1)(jsdom@25.0.1(bufferutil@4.1.0)(utf-8-validate@6.0.6))(terser@5.48.0)(tsx@4.23.0)(yaml@2.9.0) + packages/sdk-react: dependencies: '@stellar/stellar-sdk': @@ -2874,6 +2896,9 @@ packages: '@types/istanbul-reports@3.0.4': resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==} + '@types/jscodeshift@0.12.0': + resolution: {integrity: sha512-Jr2fQbEoDmjwEa92TreR/mX2t9iAaY/l5P/GKezvK4BodXahex60PDLXaQR0vAgP0KfCzc1CivHusQB9NhzX8w==} + '@types/node-forge@1.3.14': resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} @@ -3261,10 +3286,18 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-types@0.14.2: + resolution: {integrity: sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA==} + engines: {node: '>=4'} + ast-types@0.15.2: resolution: {integrity: sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==} engines: {node: '>=4'} + ast-types@0.16.1: + resolution: {integrity: sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==} + engines: {node: '>=4'} + astral-regex@1.0.0: resolution: {integrity: sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg==} engines: {node: '>=4'} @@ -5068,6 +5101,16 @@ packages: peerDependencies: '@babel/preset-env': ^7.1.6 + jscodeshift@17.4.0: + resolution: {integrity: sha512-i3ESKiiTsGynxzTg5BhsZViD0ai72/6SsI1efDZxG6/5KCoElsmquxtyhXK5lpEgoO7MTNpYkjFEdEL97SkBNg==} + engines: {node: '>=16'} + hasBin: true + peerDependencies: + '@babel/preset-env': ^7.1.6 + peerDependenciesMeta: + '@babel/preset-env': + optional: true + jsdom@24.1.3: resolution: {integrity: sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==} engines: {node: '>=18'} @@ -6247,10 +6290,18 @@ packages: readline@1.3.0: resolution: {integrity: sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg==} + recast@0.20.5: + resolution: {integrity: sha512-E5qICoPoNL4yU0H0NoBDntNB0Q5oMSNh9usFctYniLBluTthi3RsQVBXIJNbApOlvSwW/RGxIuokPcAc59J5fQ==} + engines: {node: '>= 4'} + recast@0.21.5: resolution: {integrity: sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg==} engines: {node: '>= 4'} + recast@0.23.21: + resolution: {integrity: sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==} + engines: {node: '>= 4'} + redent@3.0.0: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} @@ -6841,6 +6892,9 @@ packages: through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -6878,6 +6932,10 @@ packages: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -7437,6 +7495,10 @@ packages: resolution: {integrity: sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + write-file-atomic@5.0.1: + resolution: {integrity: sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + ws@6.2.4: resolution: {integrity: sha512-PNIUUyLI5YpkJZj60YBzX1o0ByQ4ovvfmq9N/Kig/PAYbVlGyz4R6G0SEWrD0O9acc0sT2+IdMBVLFv8FSi0Nw==} peerDependencies: @@ -10455,6 +10517,11 @@ snapshots: dependencies: '@types/istanbul-lib-report': 3.0.3 + '@types/jscodeshift@0.12.0': + dependencies: + ast-types: 0.14.2 + recast: 0.20.5 + '@types/node-forge@1.3.14': dependencies: '@types/node': 20.19.43 @@ -10901,10 +10968,18 @@ snapshots: assertion-error@2.0.1: {} + ast-types@0.14.2: + dependencies: + tslib: 2.8.1 + ast-types@0.15.2: dependencies: tslib: 2.8.1 + ast-types@0.16.1: + dependencies: + tslib: 2.8.1 + astral-regex@1.0.0: {} async-function@1.0.0: {} @@ -13110,6 +13185,31 @@ snapshots: transitivePeerDependencies: - supports-color + jscodeshift@17.4.0(@babel/preset-env@7.29.7(@babel/core@7.29.7)): + dependencies: + '@babel/core': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.29.7) + '@babel/preset-flow': 7.29.7(@babel/core@7.29.7) + '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/register': 7.29.7(@babel/core@7.29.7) + flow-parser: 0.206.0 + graceful-fs: 4.2.11 + neo-async: 2.6.2 + picocolors: 1.1.1 + picomatch: 4.0.4 + recast: 0.23.21 + tmp: 0.2.7 + write-file-atomic: 5.0.1 + optionalDependencies: + '@babel/preset-env': 7.29.7(@babel/core@7.29.7) + transitivePeerDependencies: + - supports-color + jsdom@24.1.3(bufferutil@4.1.0)(utf-8-validate@6.0.6): dependencies: cssstyle: 4.6.0 @@ -14655,6 +14755,13 @@ snapshots: readline@1.3.0: {} + recast@0.20.5: + dependencies: + ast-types: 0.14.2 + esprima: 4.0.1 + source-map: 0.6.1 + tslib: 2.8.1 + recast@0.21.5: dependencies: ast-types: 0.15.2 @@ -14662,6 +14769,14 @@ snapshots: source-map: 0.6.1 tslib: 2.8.1 + recast@0.23.21: + dependencies: + ast-types: 0.16.1 + esprima: 4.0.1 + source-map: 0.6.1 + tiny-invariant: 1.3.3 + tslib: 2.8.1 + redent@3.0.0: dependencies: indent-string: 4.0.0 @@ -15350,6 +15465,8 @@ snapshots: through@2.3.8: {} + tiny-invariant@1.3.3: {} + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -15378,6 +15495,8 @@ snapshots: dependencies: tldts-core: 6.1.86 + tmp@0.2.7: {} + tmpl@1.0.5: {} to-buffer@1.2.2: @@ -16084,6 +16203,11 @@ snapshots: imurmurhash: 0.1.4 signal-exit: 3.0.7 + write-file-atomic@5.0.1: + dependencies: + imurmurhash: 0.1.4 + signal-exit: 4.1.0 + ws@6.2.4(bufferutil@4.1.0)(utf-8-validate@6.0.6): dependencies: async-limiter: 1.0.1 From 6f7ad5f799f84ce31efee8bef5ea234440a1af52 Mon Sep 17 00:00:00 2001 From: Jerryvic911 Date: Fri, 28 Aug 2026 05:56:06 +0100 Subject: [PATCH 2/2] chore: fix prettier formatting in codemod package --- packages/codemod/README.md | 12 ++++++------ packages/codemod/test/cli.test.ts | 5 +---- packages/codemod/test/helpers.ts | 2 +- .../v1/install-react-native-polyfills.cjs | 15 ++++++--------- .../codemod/transforms/v1/typed-error-catch.cjs | 8 ++------ 5 files changed, 16 insertions(+), 26 deletions(-) diff --git a/packages/codemod/README.md b/packages/codemod/README.md index b136d6b..dd29d81 100644 --- a/packages/codemod/README.md +++ b/packages/codemod/README.md @@ -27,12 +27,12 @@ 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. | +| 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 diff --git a/packages/codemod/test/cli.test.ts b/packages/codemod/test/cli.test.ts index 7547f2c..d03e158 100644 --- a/packages/codemod/test/cli.test.ts +++ b/packages/codemod/test/cli.test.ts @@ -13,10 +13,7 @@ beforeEach(() => { const appDir = path.join(tempDir, 'app'); fs.mkdirSync(appDir, { recursive: true }); - fs.writeFileSync( - path.join(appDir, 'errors.ts'), - loadFixture('typed-error-catch', 'input.ts'), - ); + fs.writeFileSync(path.join(appDir, 'errors.ts'), loadFixture('typed-error-catch', 'input.ts')); fs.writeFileSync( path.join(appDir, 'index.tsx'), loadFixture('install-react-native-polyfills', 'input.tsx'), diff --git a/packages/codemod/test/helpers.ts b/packages/codemod/test/helpers.ts index b6d4536..2e93aa4 100644 --- a/packages/codemod/test/helpers.ts +++ b/packages/codemod/test/helpers.ts @@ -48,4 +48,4 @@ export function normalize(source: string): string { .replace(/[ \t]+$/gm, '') .replace(/\n{2,}/g, '\n') .trim(); -} \ No newline at end of file +} diff --git a/packages/codemod/transforms/v1/install-react-native-polyfills.cjs b/packages/codemod/transforms/v1/install-react-native-polyfills.cjs index 79f2217..9ff1629 100644 --- a/packages/codemod/transforms/v1/install-react-native-polyfills.cjs +++ b/packages/codemod/transforms/v1/install-react-native-polyfills.cjs @@ -32,8 +32,10 @@ module.exports = function transform(fileInfo, api, options) { const printOptions = options.printOptions || { quote: 'single' }; const importsReactNative = - root.find(j.ImportDeclaration).filter((p) => p.node.source.value === 'react-native').size() > - 0; + root + .find(j.ImportDeclaration) + .filter((p) => p.node.source.value === 'react-native') + .size() > 0; const importsSdk = root @@ -67,9 +69,7 @@ module.exports = function transform(fileInfo, api, options) { }; function ensureNamedImport(j, root, source, name) { - const existing = root - .find(j.ImportDeclaration) - .filter((p) => p.node.source.value === source); + const existing = root.find(j.ImportDeclaration).filter((p) => p.node.source.value === source); if (existing.size() > 0) { const decl = existing.paths()[0].node; @@ -82,10 +82,7 @@ function ensureNamedImport(j, root, source, name) { return; } - const newImport = j.importDeclaration( - [j.importSpecifier(j.identifier(name))], - j.literal(source), - ); + const newImport = j.importDeclaration([j.importSpecifier(j.identifier(name))], j.literal(source)); const body = root.get().node.program.body; const lastImportIndex = body.reduce( (acc, node, index) => (node.type === 'ImportDeclaration' ? index : acc), diff --git a/packages/codemod/transforms/v1/typed-error-catch.cjs b/packages/codemod/transforms/v1/typed-error-catch.cjs index bbb4b4d..d5a231d 100644 --- a/packages/codemod/transforms/v1/typed-error-catch.cjs +++ b/packages/codemod/transforms/v1/typed-error-catch.cjs @@ -132,16 +132,12 @@ function isMessageIncludesCall(node, errName) { function ensureNamedImport(j, root, source, names) { if (names.size === 0) return; - const existing = root - .find(j.ImportDeclaration) - .filter((p) => p.node.source.value === source); + const existing = root.find(j.ImportDeclaration).filter((p) => p.node.source.value === source); if (existing.size() > 0) { const decl = existing.paths()[0].node; const existingNames = new Set( - decl.specifiers - .filter((s) => s.type === 'ImportSpecifier') - .map((s) => s.imported.name), + decl.specifiers.filter((s) => s.type === 'ImportSpecifier').map((s) => s.imported.name), ); for (const name of names) { if (!existingNames.has(name)) {