diff --git a/MIGRATION_GUIDE_v5.md b/MIGRATION_GUIDE_v5.md
index 32767eb1..6bc684aa 100644
--- a/MIGRATION_GUIDE_v5.md
+++ b/MIGRATION_GUIDE_v5.md
@@ -194,3 +194,80 @@ itemWrapper={(params, makeItem, { compact }) => makeItem(params)}
// After (v5.0)
itemWrapper={(params, makeItem, { pinned }) => makeItem(params)}
```
+
+## Step-by-Step Migration
+
+### Step 1: Update Dependencies
+
+```bash
+npm install @gravity-ui/navigation@^5.0.0
+```
+
+### Step 2: Run Automated Migration
+
+The easiest way to migrate `compact` → `isExpanded` props is to use our codemod:
+
+```bash
+# Complete migration (recommended)
+npx navigation-codemod v5 src/
+```
+
+The codemod automatically handles:
+
+- **Literal values**: `compact={true}` → `isExpanded={false}`
+- **Variable references**: `compact={isCompact}` → `isExpanded={!isCompact}`
+- **Double negation removal**: `compact={!isExpanded}` → `isExpanded={isExpanded}`
+- **Complex expressions**: `compact={a && b}` → `isExpanded={!(a && b)}`
+- **Shorthand props**: `compact` → `isExpanded={false}`
+- **Destructuring in callbacks**: `({ compact })` → `({ isExpanded })`
+- **Callback parameters**: `(node, compact)` → `(node, isExpanded)`
+- **Pass-through props**: `compact={compact}` in callbacks → `isExpanded={isExpanded}` (no double inversion)
+
+### Step 3: Manual Updates (if needed)
+
+The codemod handles most cases, but you may need to manually update:
+
+#### Conditional logic using renamed variables
+
+```tsx
+// Before (v4.x)
+renderFooter={({ compact }) => (
+
...
+)}
+
+// After codemod (parameter renamed, but ternary logic needs manual fix)
+renderFooter={({ isExpanded }) => (
+ ...
// ← swap branches manually
+)}
+```
+
+#### Update CSS variable usage
+
+Replace deprecated CSS variables with zone-specific alternatives (see CSS Variables section above).
+
+### Step 4: Verify and Test
+
+1. **TypeScript Compilation**: Ensure all type errors are resolved
+2. **Runtime Testing**: Test navigation expand/collapse functionality
+3. **Visual Regression**: Check that UI appears correctly in both states
+
+## Codemod Limitations
+
+Our codemod handles most cases automatically, but may not cover:
+
+1. **Conditional expressions using renamed variables**: Ternary operators like `compact ? 'a' : 'b'` need manual logic inversion
+2. **Dynamic property access**: `item['compact']`
+3. **Computed property names**: `item[propName]`
+4. **Spread patterns with compact**: `{...props, compact: true}`
+5. **Non-target components**: Only `FooterItem`, `MobileLogo`, and `Item` components are transformed
+
+## Migration Checklist
+
+- [ ] **Dependencies**: Update to @gravity-ui/navigation@^5.0.0
+- [ ] **Run Codemod**: Execute `compact-to-is-expanded` transform on your codebase
+- [ ] **Review Conditionals**: Manually check ternary expressions using renamed variables
+- [ ] **CSS Variables**: Update deprecated CSS variable names to zone-specific alternatives
+- [ ] **Context Usage**: Update any direct usage of `AsideHeaderContext` with new prop names
+- [ ] **TypeScript**: Resolve any remaining type errors
+- [ ] **Tests**: Update test assertions and mocks
+- [ ] **Visual Testing**: Verify navigation works correctly in both expanded and collapsed states
diff --git a/codemods/bin/cli.js b/codemods/bin/cli.js
index 50a585f1..6a079cbb 100755
--- a/codemods/bin/cli.js
+++ b/codemods/bin/cli.js
@@ -9,7 +9,7 @@ const {program} = require('commander');
const PACKAGE_DIR = path.dirname(__dirname);
const TRANSFORMS_DIR = path.join(PACKAGE_DIR, 'transforms');
-const AVAILABLE_TRANSFORMS = ['v4'];
+const AVAILABLE_TRANSFORMS = ['v4', 'v5'];
// Get available transforms
const availableTransforms = fs
@@ -114,18 +114,30 @@ program
});
});
-// Default command to run v4 transforms
+// Command to run v4 transforms
program
.command('v4 ')
- .description('Run all transforms in sequence')
+ .description('Run v4 migration transforms')
.option('-d, --dry', 'Dry run (no changes will be made)')
.option('-v, --verbose', 'Verbose output')
.option('--ignore-pattern ', 'Ignore files matching this pattern')
.action((targetPath, options) => {
- console.log('Running all transforms in sequence...');
+ console.log('Running v4 migration transforms...');
runTransform('v4', targetPath, options);
});
+// Command to run v5 transforms
+program
+ .command('v5 ')
+ .description('Run v5 migration transforms (compact → isExpanded)')
+ .option('-d, --dry', 'Dry run (no changes will be made)')
+ .option('-v, --verbose', 'Verbose output')
+ .option('--ignore-pattern ', 'Ignore files matching this pattern')
+ .action((targetPath, options) => {
+ console.log('Running v5 migration transforms...');
+ runTransform('v5', targetPath, options);
+ });
+
// Help command
program
.command('help')
@@ -138,8 +150,9 @@ program
program.on('--help', () => {
console.log('');
console.log('Examples:');
- console.log(' $ navigation-codemod transform v4 ./src');
- console.log(' $ navigation-codemod transform v4 ./src --dry');
+ console.log(' $ navigation-codemod v4 ./src');
+ console.log(' $ navigation-codemod v5 ./src');
+ console.log(' $ navigation-codemod v5 ./src --dry');
console.log(' $ navigation-codemod list');
console.log('');
console.log('Available transforms:');
diff --git a/codemods/transforms/__testfixtures__/compactToIsExpanded.input.tsx b/codemods/transforms/__testfixtures__/compactToIsExpanded.input.tsx
new file mode 100644
index 00000000..006ead42
--- /dev/null
+++ b/codemods/transforms/__testfixtures__/compactToIsExpanded.input.tsx
@@ -0,0 +1,101 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+// @ts-nocheck
+import React from 'react';
+import {AsideHeader, FooterItem, MobileLogo} from '@gravity-ui/navigation';
+
+// Test 1: Literal boolean values
+function LiteralBooleans() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
+
+// Test 2: Shorthand boolean (compact without value means compact={true})
+function ShorthandBoolean() {
+ return ;
+}
+
+// Test 3: Variable reference
+function VariableReference() {
+ const isCompact = true;
+ const someState = false;
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+// Test 4: Already negated expression - should remove double negation
+function NegatedExpression() {
+ const isExpanded = true;
+ const someVar = false;
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+// Test 5: Complex expressions
+function ComplexExpressions() {
+ const a = true;
+ const b = false;
+ const getValue = () => true;
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+// Test 6: Destructuring in renderFooter callback (JSX)
+function RenderFooterCallback() {
+ return (
+ (
+
+ )}
+ />
+ );
+}
+
+// Test 7: Destructuring in renderFooter callback (object)
+const config = {
+ renderFooter: ({compact}) => {
+ return ;
+ },
+};
+
+// Test 8: Logo wrapper with second parameter
+const logoConfig = {
+ logo: {
+ wrapper: (node, compact) => {node},
+ },
+};
+
+// Test 9: collapseButtonWrapper
+function CollapseButtonWrapper() {
+ return (
+ (
+ {node}
+ )}
+ />
+ );
+}
+
+// Test 10: Non-target component should NOT be transformed
+function NonTargetComponent() {
+ return ;
+}
diff --git a/codemods/transforms/__testfixtures__/compactToIsExpanded.output.tsx b/codemods/transforms/__testfixtures__/compactToIsExpanded.output.tsx
new file mode 100644
index 00000000..e913349b
--- /dev/null
+++ b/codemods/transforms/__testfixtures__/compactToIsExpanded.output.tsx
@@ -0,0 +1,101 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+// @ts-nocheck
+import React from 'react';
+import {AsideHeader, FooterItem, MobileLogo} from '@gravity-ui/navigation';
+
+// Test 1: Literal boolean values
+function LiteralBooleans() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
+
+// Test 2: Shorthand boolean (compact without value means compact={true})
+function ShorthandBoolean() {
+ return ;
+}
+
+// Test 3: Variable reference
+function VariableReference() {
+ const isCompact = true;
+ const someState = false;
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+// Test 4: Already negated expression - should remove double negation
+function NegatedExpression() {
+ const isExpanded = true;
+ const someVar = false;
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+// Test 5: Complex expressions
+function ComplexExpressions() {
+ const a = true;
+ const b = false;
+ const getValue = () => true;
+
+ return (
+ <>
+
+
+ >
+ );
+}
+
+// Test 6: Destructuring in renderFooter callback (JSX)
+function RenderFooterCallback() {
+ return (
+ (
+
+ )}
+ />
+ );
+}
+
+// Test 7: Destructuring in renderFooter callback (object)
+const config = {
+ renderFooter: ({isExpanded}) => {
+ return ;
+ },
+};
+
+// Test 8: Logo wrapper with second parameter
+const logoConfig = {
+ logo: {
+ wrapper: (node, isExpanded) => {node},
+ },
+};
+
+// Test 9: collapseButtonWrapper
+function CollapseButtonWrapper() {
+ return (
+ (
+ {node}
+ )}
+ />
+ );
+}
+
+// Test 10: Non-target component should NOT be transformed
+function NonTargetComponent() {
+ return ;
+}
diff --git a/codemods/transforms/__testfixtures__/v5.input.tsx b/codemods/transforms/__testfixtures__/v5.input.tsx
new file mode 100644
index 00000000..a962394f
--- /dev/null
+++ b/codemods/transforms/__testfixtures__/v5.input.tsx
@@ -0,0 +1,25 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+// @ts-nocheck
+import React from 'react';
+import {AsideHeader, FooterItem, MobileLogo} from '@gravity-ui/navigation';
+
+// Basic FooterItem with compact prop
+function BasicExample() {
+ return ;
+}
+
+// MobileLogo with compact prop
+function MobileExample() {
+ return ;
+}
+
+// renderFooter callback with destructuring
+function CallbackExample() {
+ return (
+ (
+
+ )}
+ />
+ );
+}
diff --git a/codemods/transforms/__testfixtures__/v5.output.tsx b/codemods/transforms/__testfixtures__/v5.output.tsx
new file mode 100644
index 00000000..e53b7615
--- /dev/null
+++ b/codemods/transforms/__testfixtures__/v5.output.tsx
@@ -0,0 +1,25 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+// @ts-nocheck
+import React from 'react';
+import {AsideHeader, FooterItem, MobileLogo} from '@gravity-ui/navigation';
+
+// Basic FooterItem with compact prop
+function BasicExample() {
+ return ;
+}
+
+// MobileLogo with compact prop
+function MobileExample() {
+ return ;
+}
+
+// renderFooter callback with destructuring
+function CallbackExample() {
+ return (
+ (
+
+ )}
+ />
+ );
+}
diff --git a/codemods/transforms/__tests__/compactToIsExpanded.test.ts b/codemods/transforms/__tests__/compactToIsExpanded.test.ts
new file mode 100644
index 00000000..f8edf3db
--- /dev/null
+++ b/codemods/transforms/__tests__/compactToIsExpanded.test.ts
@@ -0,0 +1,7 @@
+import {defineTest} from 'jscodeshift/src/testUtils';
+
+const testName = 'compactToIsExpanded';
+
+defineTest(__dirname, testName, null, testName, {
+ parser: 'tsx',
+});
diff --git a/codemods/transforms/__tests__/v5.test.ts b/codemods/transforms/__tests__/v5.test.ts
new file mode 100644
index 00000000..b4646c24
--- /dev/null
+++ b/codemods/transforms/__tests__/v5.test.ts
@@ -0,0 +1,7 @@
+import {defineTest} from 'jscodeshift/src/testUtils';
+
+const testName = 'v5';
+
+defineTest(__dirname, testName, null, testName, {
+ parser: 'tsx',
+});
diff --git a/codemods/transforms/compactToIsExpanded.ts b/codemods/transforms/compactToIsExpanded.ts
new file mode 100644
index 00000000..71345b0a
--- /dev/null
+++ b/codemods/transforms/compactToIsExpanded.ts
@@ -0,0 +1,345 @@
+/* eslint-disable no-param-reassign */
+import {execSync} from 'child_process';
+
+import type {
+ API,
+ ASTPath,
+ ArrowFunctionExpression,
+ Collection,
+ FileInfo,
+ FunctionExpression,
+ JSCodeshift,
+ JSXAttribute,
+ JSXIdentifier,
+ ObjectPattern,
+ ObjectProperty,
+} from 'jscodeshift';
+
+/**
+ * Codemod to transform `compact` prop to `isExpanded` with inverted boolean logic.
+ *
+ * Transformations:
+ * - compact={true} → isExpanded={false}
+ * - compact={false} → isExpanded={true}
+ * - compact (shorthand) → isExpanded={false}
+ * - compact={someVar} → isExpanded={!someVar}
+ * - compact={!someVar} → isExpanded={someVar}
+ * - compact={expression} → isExpanded={!(expression)}
+ *
+ * Special case for destructured parameters:
+ * - renderFooter={({ compact }) => }
+ * - → renderFooter={({ isExpanded }) => }
+ * - (No inversion when the value is the renamed destructured parameter)
+ *
+ * Also handles:
+ * - Function parameter destructuring: ({ compact }) => → ({ isExpanded }) =>
+ * - Callback second parameter: (node, compact) => → (node, isExpanded) =>
+ */
+
+const TARGET_JSX_COMPONENTS = new Set(['FooterItem', 'MobileLogo', 'Item']);
+
+const RENDER_CALLBACK_PROPS = new Set(['renderFooter', 'collapseButtonWrapper']);
+
+export default function transformer(file: FileInfo, api: API) {
+ const j = api.jscodeshift;
+ const root = j(file.source);
+
+ // First pass: rename destructuring parameters and callback params
+ const hasDestructuringUpdate = updateDestructuringParams(root, j);
+ const hasCallbackParamUpdate = updateCallbackSecondParam(root, j);
+
+ // Second pass: update JSX props with awareness of renamed identifiers
+ const hasJsxPropsUpdate = updateJsxProps(root, j);
+
+ if (!hasJsxPropsUpdate && !hasDestructuringUpdate && !hasCallbackParamUpdate) {
+ return null;
+ }
+
+ const output = root.toSource();
+
+ // Format with Prettier to ensure consistent output
+ // Uses project's prettier config if available (searches from cwd)
+ // Falls back to unformatted output if Prettier is not installed or fails
+ try {
+ const formatted = execSync('prettier --parser typescript', {
+ input: output,
+ encoding: 'utf-8',
+ maxBuffer: 10 * 1024 * 1024,
+ stdio: ['pipe', 'pipe', 'pipe'],
+ });
+ return formatted;
+ } catch {
+ // Prettier not installed or failed - return unformatted output
+ // User can run their own formatter afterwards
+ return output;
+ }
+}
+
+/**
+ * Inverts a JSX expression value.
+ * - true → false
+ * - false → true
+ * - someVar → !someVar
+ * - !someVar → someVar
+ * - (expression) → !(expression)
+ */
+function invertExpression(j: JSCodeshift, expr: any): any {
+ // Handle literal booleans - modify in place to preserve formatting
+ if (j.BooleanLiteral.check(expr) || j.Literal.check(expr)) {
+ const value = expr.value;
+ if (typeof value === 'boolean') {
+ expr.value = !value;
+ return expr;
+ }
+ }
+
+ // Handle unary negation: !something → something
+ if (j.UnaryExpression.check(expr) && expr.operator === '!') {
+ return expr.argument;
+ }
+
+ // Handle identifiers and other expressions: something → !something
+ return j.unaryExpression('!', expr);
+}
+
+/**
+ * Update JSX props: compact → isExpanded with inverted value
+ * Special handling: if value is an identifier that was renamed from destructuring,
+ * just rename it without inversion.
+ */
+function updateJsxProps(root: Collection, j: JSCodeshift): boolean {
+ let hasChanges = false;
+
+ root.find(j.JSXAttribute, {
+ name: {type: 'JSXIdentifier', name: 'compact'},
+ }).forEach((path: ASTPath) => {
+ const jsxElement = path.parentPath.parentPath;
+
+ // Check if this is on a target component
+ if (jsxElement && j.JSXOpeningElement.check(jsxElement.node)) {
+ const elementName = jsxElement.node.name;
+ let componentName: string | null = null;
+
+ if (j.JSXIdentifier.check(elementName)) {
+ componentName = elementName.name;
+ }
+
+ // Only transform if it's a target component or if we can't determine
+ if (componentName && !TARGET_JSX_COMPONENTS.has(componentName)) {
+ return;
+ }
+ }
+
+ const attrNode = path.node;
+ const attrValue = attrNode.value;
+
+ // Rename compact → isExpanded
+ (attrNode.name as JSXIdentifier).name = 'isExpanded';
+
+ if (attrValue === null) {
+ // Shorthand: compact → isExpanded={false}
+ attrNode.value = j.jsxExpressionContainer(j.booleanLiteral(false));
+ hasChanges = true;
+ } else if (j.JSXExpressionContainer.check(attrValue)) {
+ const expr = attrValue.expression;
+
+ if (!j.JSXEmptyExpression.check(expr)) {
+ // Check if this is a simple identifier named 'compact'
+ // that might be from a renamed destructuring parameter
+ if (j.Identifier.check(expr) && expr.name === 'compact') {
+ // Just rename it to isExpanded in place (no inversion)
+ // This handles the case: compact={compact} → isExpanded={isExpanded}
+ expr.name = 'isExpanded';
+ } else if (j.Identifier.check(expr) && expr.name === 'isExpanded') {
+ // Already renamed by destructuring pass, leave as is
+ } else {
+ // Invert the expression
+ attrValue.expression = invertExpression(j, expr);
+ }
+ }
+ hasChanges = true;
+ } else if (j.StringLiteral.check(attrValue)) {
+ // Edge case: compact="true" or compact="false" (unusual but handle it)
+ const strValue = attrValue.value;
+ if (strValue === 'true') {
+ attrNode.value = j.jsxExpressionContainer(j.booleanLiteral(false));
+ hasChanges = true;
+ } else if (strValue === 'false') {
+ attrNode.value = j.jsxExpressionContainer(j.booleanLiteral(true));
+ hasChanges = true;
+ }
+ }
+ });
+
+ return hasChanges;
+}
+
+/**
+ * Update destructuring parameters in render callbacks:
+ * renderFooter={({ compact }) => ...} → renderFooter={({ isExpanded }) => ...}
+ */
+function updateDestructuringParams(root: Collection, j: JSCodeshift): boolean {
+ let hasChanges = false;
+
+ // Find JSX attributes that are render callbacks
+ root.find(j.JSXAttribute).forEach((attrPath: ASTPath) => {
+ const attrName = attrPath.node.name;
+ if (!j.JSXIdentifier.check(attrName)) return;
+
+ if (!RENDER_CALLBACK_PROPS.has(attrName.name)) return;
+
+ const attrValue = attrPath.node.value;
+ if (!j.JSXExpressionContainer.check(attrValue)) return;
+
+ const expr = attrValue.expression;
+ if (!j.ArrowFunctionExpression.check(expr) && !j.FunctionExpression.check(expr)) return;
+
+ const funcExpr = expr as ArrowFunctionExpression | FunctionExpression;
+ const params = funcExpr.params;
+
+ // Look for destructuring pattern with 'compact'
+ params.forEach((param) => {
+ if (j.ObjectPattern.check(param)) {
+ const objPattern = param as ObjectPattern;
+ objPattern.properties.forEach((prop) => {
+ if (j.ObjectProperty.check(prop) || j.Property.check(prop)) {
+ const key = (prop as ObjectProperty).key;
+ if (j.Identifier.check(key) && key.name === 'compact') {
+ key.name = 'isExpanded';
+ hasChanges = true;
+
+ // Also update the value if it's a shorthand (compact → isExpanded)
+ const value = (prop as ObjectProperty).value;
+ if (j.Identifier.check(value) && value.name === 'compact') {
+ value.name = 'isExpanded';
+
+ // Rename all references to 'compact' in the function body to 'isExpanded'
+ // and invert ternary expressions where compact is the test
+ j(funcExpr)
+ .find(j.Identifier, {name: 'compact'})
+ .forEach((identPath) => {
+ // Skip if this is the parameter itself
+ if (identPath.value !== value) {
+ identPath.value.name = 'isExpanded';
+
+ // Check if this identifier is the test of a conditional expression
+ if (
+ identPath.parent &&
+ j.ConditionalExpression.check(
+ identPath.parent.value,
+ ) &&
+ identPath.parent.value.test === identPath.value
+ ) {
+ // Swap consequent and alternate to invert the logic
+ const temp = identPath.parent.value.consequent;
+ identPath.parent.value.consequent =
+ identPath.parent.value.alternate;
+ identPath.parent.value.alternate = temp;
+ }
+ }
+ });
+ }
+ }
+ }
+ });
+ }
+ });
+ });
+
+ // Also find object properties that are render callbacks
+ root.find(j.ObjectProperty).forEach((propPath: ASTPath) => {
+ const propKey = propPath.node.key;
+ if (!j.Identifier.check(propKey)) return;
+
+ if (!RENDER_CALLBACK_PROPS.has(propKey.name) && propKey.name !== 'wrapper') return;
+
+ const propValue = propPath.node.value;
+ if (!j.ArrowFunctionExpression.check(propValue) && !j.FunctionExpression.check(propValue))
+ return;
+
+ const funcExpr = propValue as ArrowFunctionExpression | FunctionExpression;
+ const params = funcExpr.params;
+
+ // Look for destructuring pattern with 'compact'
+ params.forEach((param) => {
+ if (j.ObjectPattern.check(param)) {
+ const objPattern = param as ObjectPattern;
+ objPattern.properties.forEach((prop) => {
+ if (j.ObjectProperty.check(prop) || j.Property.check(prop)) {
+ const key = (prop as ObjectProperty).key;
+ if (j.Identifier.check(key) && key.name === 'compact') {
+ key.name = 'isExpanded';
+ hasChanges = true;
+
+ const value = (prop as ObjectProperty).value;
+ if (j.Identifier.check(value) && value.name === 'compact') {
+ value.name = 'isExpanded';
+
+ // Rename all references to 'compact' in the function body to 'isExpanded'
+ // and invert ternary expressions where compact is the test
+ j(funcExpr)
+ .find(j.Identifier, {name: 'compact'})
+ .forEach((identPath) => {
+ // Skip if this is the parameter itself
+ if (identPath.value !== value) {
+ identPath.value.name = 'isExpanded';
+
+ // Check if this identifier is the test of a conditional expression
+ if (
+ identPath.parent &&
+ j.ConditionalExpression.check(
+ identPath.parent.value,
+ ) &&
+ identPath.parent.value.test === identPath.value
+ ) {
+ // Swap consequent and alternate to invert the logic
+ const temp = identPath.parent.value.consequent;
+ identPath.parent.value.consequent =
+ identPath.parent.value.alternate;
+ identPath.parent.value.alternate = temp;
+ }
+ }
+ });
+ }
+ }
+ }
+ });
+ }
+ });
+ });
+
+ return hasChanges;
+}
+
+/**
+ * Update callback second parameter:
+ * wrapper: (node, compact) => ... → wrapper: (node, isExpanded) => ...
+ */
+function updateCallbackSecondParam(root: Collection, j: JSCodeshift): boolean {
+ let hasChanges = false;
+
+ // Find 'wrapper' property in objects (e.g., logo={{ wrapper: (node, compact) => ... }})
+ root.find(j.ObjectProperty, {
+ key: {type: 'Identifier', name: 'wrapper'},
+ }).forEach((propPath: ASTPath) => {
+ const propValue = propPath.node.value;
+
+ if (!j.ArrowFunctionExpression.check(propValue) && !j.FunctionExpression.check(propValue)) {
+ return;
+ }
+
+ const funcExpr = propValue as ArrowFunctionExpression | FunctionExpression;
+ const params = funcExpr.params;
+
+ // Check if second parameter is 'compact'
+ if (params.length >= 2) {
+ const secondParam = params[1];
+ if (j.Identifier.check(secondParam) && secondParam.name === 'compact') {
+ secondParam.name = 'isExpanded';
+ hasChanges = true;
+ }
+ }
+ });
+
+ return hasChanges;
+}
diff --git a/codemods/transforms/v5.ts b/codemods/transforms/v5.ts
new file mode 100644
index 00000000..69dffb2a
--- /dev/null
+++ b/codemods/transforms/v5.ts
@@ -0,0 +1,32 @@
+import type {API, FileInfo, Options} from 'jscodeshift';
+
+import compactToIsExpanded from './compactToIsExpanded';
+
+export default function transform(file: FileInfo, api: API, options: Options) {
+ let source = file.source;
+ let hasChanges = false;
+
+ const transforms = [{name: 'compact-to-is-expanded', transform: compactToIsExpanded}];
+
+ for (const {name, transform: transformFn} of transforms) {
+ try {
+ const result = transformFn({source, path: file.path}, api);
+ if (result && result !== source) {
+ source = result;
+ hasChanges = true;
+
+ if (options && options.verbose) {
+ console.log(`✓ Applied ${name}`);
+ }
+ }
+ } catch (error) {
+ const errorMessage = error instanceof Error ? error.message : String(error);
+ console.error(`Error applying ${name}:`, errorMessage);
+ if (options && options.verbose && error instanceof Error) {
+ console.error(error.stack);
+ }
+ }
+ }
+
+ return hasChanges ? source : null;
+}