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
77 changes: 77 additions & 0 deletions MIGRATION_GUIDE_v5.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => (
<div className={compact ? 'collapsed' : 'expanded'}>...</div>
)}

// After codemod (parameter renamed, but ternary logic needs manual fix)
renderFooter={({ isExpanded }) => (
<div className={isExpanded ? 'expanded' : 'collapsed'}>...</div> // ← 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
25 changes: 19 additions & 6 deletions codemods/bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -114,18 +114,30 @@ program
});
});

// Default command to run v4 transforms
// Command to run v4 transforms
program
.command('v4 <path>')
.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 <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 <path>')
.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 <pattern>', 'Ignore files matching this pattern')
.action((targetPath, options) => {
console.log('Running v5 migration transforms...');
runTransform('v5', targetPath, options);
});

// Help command
program
.command('help')
Expand All @@ -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:');
Expand Down
101 changes: 101 additions & 0 deletions codemods/transforms/__testfixtures__/compactToIsExpanded.input.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<FooterItem compact={true} id="item1" title="Item 1" />
<FooterItem compact={false} id="item2" title="Item 2" />
<MobileLogo compact={true} text="Logo" />
</>
);
}

// Test 2: Shorthand boolean (compact without value means compact={true})
function ShorthandBoolean() {
return <FooterItem compact id="item3" title="Item 3" />;
}

// Test 3: Variable reference
function VariableReference() {
const isCompact = true;
const someState = false;

return (
<>
<FooterItem compact={isCompact} id="item4" title="Item 4" />
<FooterItem compact={someState} id="item5" title="Item 5" />
</>
);
}

// Test 4: Already negated expression - should remove double negation
function NegatedExpression() {
const isExpanded = true;
const someVar = false;

return (
<>
<FooterItem compact={!isExpanded} id="item6" title="Item 6" />
<FooterItem compact={!someVar} id="item7" title="Item 7" />
</>
);
}

// Test 5: Complex expressions
function ComplexExpressions() {
const a = true;
const b = false;
const getValue = () => true;

return (
<>
<FooterItem compact={a && b} id="item8" title="Item 8" />
<FooterItem compact={getValue()} id="item9" title="Item 9" />
</>
);
}

// Test 6: Destructuring in renderFooter callback (JSX)
function RenderFooterCallback() {
return (
<AsideHeader
renderFooter={({compact}) => (
<FooterItem compact={compact} id="footer" title="Footer" />
)}
/>
);
}

// Test 7: Destructuring in renderFooter callback (object)
const config = {
renderFooter: ({compact}) => {
return <FooterItem compact={compact} id="footer" title="Footer" />;
},
};

// Test 8: Logo wrapper with second parameter
const logoConfig = {
logo: {
wrapper: (node, compact) => <a href="/">{node}</a>,
},
};

// Test 9: collapseButtonWrapper
function CollapseButtonWrapper() {
return (
<AsideHeader
collapseButtonWrapper={(node, {compact}) => (
<div className={compact ? 'collapsed' : 'expanded'}>{node}</div>
)}
/>
);
}

// Test 10: Non-target component should NOT be transformed
function NonTargetComponent() {
return <SomeOtherComponent compact={true} />;
}
101 changes: 101 additions & 0 deletions codemods/transforms/__testfixtures__/compactToIsExpanded.output.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<>
<FooterItem isExpanded={false} id="item1" title="Item 1" />
<FooterItem isExpanded={true} id="item2" title="Item 2" />
<MobileLogo isExpanded={false} text="Logo" />
</>
);
}

// Test 2: Shorthand boolean (compact without value means compact={true})
function ShorthandBoolean() {
return <FooterItem isExpanded={false} id="item3" title="Item 3" />;
}

// Test 3: Variable reference
function VariableReference() {
const isCompact = true;
const someState = false;

return (
<>
<FooterItem isExpanded={!isCompact} id="item4" title="Item 4" />
<FooterItem isExpanded={!someState} id="item5" title="Item 5" />
</>
);
}

// Test 4: Already negated expression - should remove double negation
function NegatedExpression() {
const isExpanded = true;
const someVar = false;

return (
<>
<FooterItem isExpanded={isExpanded} id="item6" title="Item 6" />
<FooterItem isExpanded={someVar} id="item7" title="Item 7" />
</>
);
}

// Test 5: Complex expressions
function ComplexExpressions() {
const a = true;
const b = false;
const getValue = () => true;

return (
<>
<FooterItem isExpanded={!(a && b)} id="item8" title="Item 8" />
<FooterItem isExpanded={!getValue()} id="item9" title="Item 9" />
</>
);
}

// Test 6: Destructuring in renderFooter callback (JSX)
function RenderFooterCallback() {
return (
<AsideHeader
renderFooter={({isExpanded}) => (
<FooterItem isExpanded={isExpanded} id="footer" title="Footer" />
)}
/>
);
}

// Test 7: Destructuring in renderFooter callback (object)
const config = {
renderFooter: ({isExpanded}) => {
return <FooterItem isExpanded={isExpanded} id="footer" title="Footer" />;
},
};

// Test 8: Logo wrapper with second parameter
const logoConfig = {
logo: {
wrapper: (node, isExpanded) => <a href="/">{node}</a>,
},
};

// Test 9: collapseButtonWrapper
function CollapseButtonWrapper() {
return (
<AsideHeader
collapseButtonWrapper={(node, {isExpanded}) => (
<div className={isExpanded ? 'expanded' : 'collapsed'}>{node}</div>
)}
/>
);
}

// Test 10: Non-target component should NOT be transformed
function NonTargetComponent() {
return <SomeOtherComponent compact={true} />;
}
25 changes: 25 additions & 0 deletions codemods/transforms/__testfixtures__/v5.input.tsx
Original file line number Diff line number Diff line change
@@ -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 <FooterItem compact={true} id="item1" title="Item 1" />;
}

// MobileLogo with compact prop
function MobileExample() {
return <MobileLogo compact={false} text="Logo" />;
}

// renderFooter callback with destructuring
function CallbackExample() {
return (
<AsideHeader
renderFooter={({compact}) => (
<FooterItem compact={compact} id="footer" title="Footer" />
)}
/>
);
}
25 changes: 25 additions & 0 deletions codemods/transforms/__testfixtures__/v5.output.tsx
Original file line number Diff line number Diff line change
@@ -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 <FooterItem isExpanded={false} id="item1" title="Item 1" />;
}

// MobileLogo with compact prop
function MobileExample() {
return <MobileLogo isExpanded={true} text="Logo" />;
}

// renderFooter callback with destructuring
function CallbackExample() {
return (
<AsideHeader
renderFooter={({isExpanded}) => (
<FooterItem isExpanded={isExpanded} id="footer" title="Footer" />
)}
/>
);
}
7 changes: 7 additions & 0 deletions codemods/transforms/__tests__/compactToIsExpanded.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {defineTest} from 'jscodeshift/src/testUtils';

const testName = 'compactToIsExpanded';

defineTest(__dirname, testName, null, testName, {
parser: 'tsx',
});
Loading