Files
2026-05-07 13:09:23 +02:00

190 lines
5.6 KiB
JavaScript

#!/usr/bin/env node
import type { Transform } from 'codemod:ast-grep';
import type JSONLang from 'codemod:ast-grep/langs/json';
const RNTL_VERSION = '^14.0.0-0';
const TEST_RENDERER_FALLBACK_VERSION = '^1.2.0';
const TEST_RENDERER_VERSION_BY_REACT_MINOR: Record<string, string> = {
'19.0': '^1.0.0',
'19.1': '^1.1.0',
'19.2': '^1.2.0',
};
interface PackageJson {
dependencies?: Record<string, string>;
devDependencies?: Record<string, string>;
peerDependencies?: Record<string, string>;
optionalDependencies?: Record<string, string>;
[key: string]: unknown;
}
export default async function transform(
root: Parameters<Transform<JSONLang>>[0],
): Promise<string | null> {
const filename = root.filename();
if (!isPackageJsonFile(filename)) {
return null;
}
try {
const content = root.root().text();
const packageJson: PackageJson = JSON.parse(content);
if (!hasRntlOrTestRenderer(packageJson)) {
return null;
}
let hasChanges = false;
if (removeObsoletePackages(packageJson)) {
hasChanges = true;
}
if (ensureDevDependenciesObjectExists(packageJson)) {
hasChanges = true;
}
if (ensureRntlInDevDependencies(packageJson)) {
hasChanges = true;
}
if (updateTestRendererVersionInDevDependencies(packageJson)) {
hasChanges = true;
}
if (hasChanges) {
return JSON.stringify(packageJson, null, 2) + '\n';
}
return null;
} catch (error) {
// Re-throw error to let the codemod platform handle it
// This provides better error reporting than silently returning null
throw new Error(
`Error processing ${filename}: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
function isPackageJsonFile(filename: string): boolean {
return filename.endsWith('package.json');
}
function hasRntlOrTestRenderer(packageJson: PackageJson): boolean {
const hasRntl =
packageJson.dependencies?.['@testing-library/react-native'] ||
packageJson.devDependencies?.['@testing-library/react-native'] ||
packageJson.peerDependencies?.['@testing-library/react-native'];
const hasTestRenderer =
packageJson.dependencies?.['test-renderer'] ||
packageJson.devDependencies?.['test-renderer'] ||
packageJson.peerDependencies?.['test-renderer'];
return hasRntl || hasTestRenderer;
}
function removePackageFromAllDependencyTypes(pkgName: string, packageJson: PackageJson): boolean {
let removed = false;
(
['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies'] as const
).forEach((depType) => {
if (packageJson[depType]?.[pkgName]) {
delete packageJson[depType]![pkgName];
removed = true;
if (Object.keys(packageJson[depType]!).length === 0) {
delete packageJson[depType];
}
}
});
return removed;
}
function removeEmptyDependencyObject(packageJson: PackageJson, depType: keyof PackageJson): void {
const deps = packageJson[depType];
if (deps && typeof deps === 'object' && !Array.isArray(deps) && !Object.keys(deps).length) {
delete packageJson[depType];
}
}
function ensureDevDependenciesObjectExists(packageJson: PackageJson): boolean {
if (!packageJson.devDependencies) {
packageJson.devDependencies = {};
return true;
}
return false;
}
function removeObsoletePackages(packageJson: PackageJson): boolean {
const removedTypes = removePackageFromAllDependencyTypes(
'@types/react-test-renderer',
packageJson,
);
const removedRenderer = removePackageFromAllDependencyTypes('react-test-renderer', packageJson);
return removedTypes || removedRenderer;
}
function ensureRntlInDevDependencies(packageJson: PackageJson): boolean {
let hasChanges = false;
const rntlInDeps = packageJson.dependencies?.['@testing-library/react-native'];
if (rntlInDeps) {
delete packageJson.dependencies!['@testing-library/react-native'];
removeEmptyDependencyObject(packageJson, 'dependencies');
hasChanges = true;
}
const currentVersion = packageJson.devDependencies?.['@testing-library/react-native'];
if (currentVersion !== RNTL_VERSION) {
packageJson.devDependencies!['@testing-library/react-native'] = RNTL_VERSION;
hasChanges = true;
}
return hasChanges;
}
function updateTestRendererVersionInDevDependencies(packageJson: PackageJson): boolean {
const expectedVersion = getTestRendererVersion(packageJson);
const currentVersion = packageJson.devDependencies?.['test-renderer'];
if (currentVersion !== expectedVersion) {
packageJson.devDependencies!['test-renderer'] = expectedVersion;
return true;
}
return false;
}
function getTestRendererVersion(packageJson: PackageJson): string {
const reactVersion = getPackageVersion(packageJson, 'react');
const reactMinor = reactVersion ? getKnownReact19Minor(reactVersion) : null;
return reactMinor
? TEST_RENDERER_VERSION_BY_REACT_MINOR[reactMinor]
: TEST_RENDERER_FALLBACK_VERSION;
}
function getPackageVersion(packageJson: PackageJson, pkgName: string): string | undefined {
for (const depType of [
'dependencies',
'devDependencies',
'peerDependencies',
'optionalDependencies',
] as const) {
const version = packageJson[depType]?.[pkgName];
if (version) {
return version;
}
}
return undefined;
}
function getKnownReact19Minor(version: string): string | null {
const trimmedVersion = version.trim();
const exactVersionMatch = trimmedVersion.match(/^=?~?(19\.(?:0|1|2))(?:\.\d+)?(?:-[\w.-]+)?$/);
const xRangeMatch = trimmedVersion.match(/^~?(19\.(?:0|1|2))\.(?:x|\*)$/i);
return exactVersionMatch?.[1] ?? xRangeMatch?.[1] ?? null;
}