Skip to content

Commit

Permalink
[codemod] Add a new utility to rename imports (#14919)
Browse files Browse the repository at this point in the history
Signed-off-by: Flavien DELANGLE <[email protected]>
Co-authored-by: Nora <[email protected]>
  • Loading branch information
flaviendelangle and noraleonte authored Oct 11, 2024
1 parent ad83347 commit 185dd93
Show file tree
Hide file tree
Showing 10 changed files with 292 additions and 165 deletions.
176 changes: 176 additions & 0 deletions packages/x-codemod/src/util/renameImports.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import type {
ASTPath,
Collection,
ImportDeclaration,
ImportSpecifier,
JSCodeshift,
} from 'jscodeshift';

interface ImportConfig {
oldEndpoint?: string;
newEndpoint?: string;
skipRoot?: boolean;
importsMapping: Record<string, string>;
}

interface RenameImportsParameters {
j: JSCodeshift;
root: Collection<any>;
packageNames: string[];
imports: ImportConfig[];
}

const getPathStrFromPath = (path: ASTPath<ImportDeclaration> | ASTPath<ImportSpecifier>) => {
let cleanPath: ASTPath<ImportDeclaration>;
if (path.get('type').value === 'ImportDeclaration') {
cleanPath = path as ASTPath<ImportDeclaration>;
} else {
cleanPath = path.parentPath.parentPath as ASTPath<ImportDeclaration>;
}

return cleanPath.node.source.value?.toString() ?? '';
};

const getRelativeEndpointFromPathStr = (pathStr: string, packageNames: string[]) => {
return pathStr.replace(new RegExp(`^(${packageNames.join('|')})/`), '');
};

const getMatchingNestedImport = (
path: ASTPath<ImportSpecifier> | ASTPath<ImportDeclaration>,
parameters: RenameImportsParameters,
) => {
const pathStr = getPathStrFromPath(path);
const relativeEndpoint = getRelativeEndpointFromPathStr(pathStr, parameters.packageNames);
return parameters.imports.find((importConfig) => importConfig.oldEndpoint === relativeEndpoint);
};

const getMatchingRootImport = (
path: ASTPath<ImportSpecifier>,
parameters: RenameImportsParameters,
) => {
return parameters.imports.find((importConfig) => {
return (
!importConfig.skipRoot && importConfig.importsMapping.hasOwnProperty(path.node.imported.name)
);
});
};

export function renameImports(parameters: RenameImportsParameters) {
const { j, root } = parameters;

const renamedIdentifiersMap: Record<string, string> = {};

const importDeclarations = root
// Find all the import declarations (import { ... } from '...')
.find(j.ImportDeclaration);

// Rename the nested imports specifiers
// - import { A } from '@mui/x-date-pickers/A'
// + import { B } from '@mui/x-date-pickers/A'
const nestedImportRegExp = new RegExp(`^(${parameters.packageNames.join('|')})/(.*)$`);
importDeclarations
// Filter out the declarations that are not nested endpoints of the matching packages or that don't have any update to apply
.filter((path) => {
const pathStr = getPathStrFromPath(path);
if (!pathStr.match(nestedImportRegExp)) {
return false;
}

return !!getMatchingNestedImport(path, parameters);
})
// Find all the import specifiers (extract A in import { A } from '...')
.find(j.ImportSpecifier)
// Filter out the specifiers that don't need to be updated
.filter((path) => {
return getMatchingNestedImport(path, parameters)!.importsMapping.hasOwnProperty(
path.node.imported.name,
);
})
// Rename the import specifiers
.replaceWith((path) => {
const newName = getMatchingNestedImport(path, parameters)!.importsMapping[
path.node.imported.name
];

// If the import is alias, we keep the alias and don't rename the variable usage
const hasAlias = path.node.local?.name !== path.node.imported.name;
if (hasAlias) {
return j.importSpecifier(j.identifier(newName), j.identifier(path.node.local!.name));
}

renamedIdentifiersMap[path.node.imported.name] = newName;
return j.importSpecifier(j.identifier(newName));
});

// Rename the root imports specifiers
// - import { A } from '@mui/x-date-pickers'
// + import { B } from '@mui/x-date-pickers'
const rootImportRegExp = new RegExp(`^(${parameters.packageNames.join('|')})$`);
importDeclarations
// Filter out the declarations that are not root endpoint of the matching packages
.filter((path) => {
const pathStr = getPathStrFromPath(path);
return !!pathStr.match(rootImportRegExp);
})
.find(j.ImportSpecifier)
.filter((path) => {
return !!getMatchingRootImport(path, parameters);
})
// Rename the import specifiers
.replaceWith((path) => {
const newName = getMatchingRootImport(path, parameters)!.importsMapping[
path.node.imported.name
];

// If the import is alias, we keep the alias and don't rename the variable usage
const hasAlias = path.node.local?.name !== path.node.imported.name;
if (hasAlias) {
return j.importSpecifier(j.identifier(newName), j.identifier(path.node.local!.name));
}

renamedIdentifiersMap[path.node.imported.name] = newName;
return j.importSpecifier(j.identifier(newName));
});

// Rename the nested import declarations
// - import { B } from '@mui/x-date-pickers/A'
// + import { B } from '@mui/x-date-pickers/B'
importDeclarations
// Filter out the declarations that are not nested endpoints of the matching packages or that don't have any update to apply
.filter((path) => {
const pathStr = getPathStrFromPath(path);
if (!pathStr.match(nestedImportRegExp)) {
return false;
}

return !!getMatchingNestedImport(path, parameters)?.newEndpoint;
})
.replaceWith((path) => {
const pathStr = getPathStrFromPath(path);
const oldEndpoint = getRelativeEndpointFromPathStr(pathStr, parameters.packageNames);
const newEndpoint = getMatchingNestedImport(path, parameters)!.newEndpoint;
const newPathStr = pathStr.replace(oldEndpoint, newEndpoint!);

return j.importDeclaration(
// Copy over the existing import specifiers
path.node.specifiers,
// Replace the source with our new source
j.stringLiteral(newPathStr),
);
});

// Rename the import usage
// - <A />
// + <B />
root
.find(j.Identifier)
.filter((path) => {
return renamedIdentifiersMap.hasOwnProperty(path.node.name);
})
.replaceWith((path) => {
const newName = renamedIdentifiersMap[path.node.name];
return j.identifier(newName);
});

return root;
}
156 changes: 70 additions & 86 deletions packages/x-codemod/src/v6.0.0/pickers/view-components-rename/index.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,6 @@
import { ASTPath, ImportDeclaration } from 'jscodeshift';
import type { JsCodeShiftAPI, JsCodeShiftFileInfo } from '../../../types';
import { renameImports } from '../../../util/renameImports';

const SUB_PACKAGES = {
CalendarPicker: 'DateCalendar',
CalendarPickerSkeleton: 'DayCalendarSkeleton',
MonthPicker: 'MonthCalendar',
YearPicker: 'YearCalendar',
ClockPicker: 'TimeClock',
};

const VARIABLES = {
// Date Calendar
CalendarPicker: 'DateCalendar',
CalendarPickerProps: 'DateCalendarProps',
CalendarPickerSlotsComponent: 'DateCalendarSlotsComponent',
CalendarPickerSlotsComponentsProps: 'DateCalendarSlotsComponentsProps',
CalendarPickerClasses: 'DateCalendarClasses',
CalendarPickerClassKey: 'DateCalendarClassKey',
calendarPickerClasses: 'dateCalendarClasses',
getCalendarPickerUtilityClass: 'getDateCalendarUtilityClass',

// Month Calendar
MonthPicker: 'MonthCalendar',
MonthPickerProps: 'MonthCalendarProps',
MonthPickerClasses: 'MonthCalendarClasses',
MonthPickerClassKey: 'MonthCalendarClassKey',
monthPickerClasses: 'monthCalendarClasses',
getMonthPickerUtilityClass: 'getMonthCalendarUtilityClass',

YearPicker: 'YearCalendar',
YearPickerProps: 'YearCalendarProps',
YearPickerClasses: 'YearCalendarClasses',
YearPickerClassKey: 'YearCalendarClassKey',
yearPickerClasses: 'yearCalendarClasses',
getYearPickerUtilityClass: 'getYearCalendarUtilityClass',

ClockPicker: 'TimeClock',
ClockPickerProps: 'TimeClockProps',
ClockPickerClasses: 'TimeClockClasses',
ClockPickerClassKey: 'TimeClockClassKey',
clockPickerClasses: 'timeClockClasses',
getClockPickerUtilityClass: 'getTimeClockUtilityClass',

CalendarPickerSkeleton: 'DayCalendarSkeleton',
CalendarPickerSkeletonProps: 'DayCalendarSkeletonProps',
CalendarPickerSkeletonClasses: 'DayCalendarSkeletonClasses',
CalendarPickerSkeletonClassKey: 'DayCalendarSkeletonClassKey',
calendarPickerSkeletonClasses: 'dayCalendarSkeletonClasses',
getCalendarPickerSkeletonUtilityClass: 'getDayCalendarSkeletonUtilityClass',
};

const PACKAGE_REGEXP = /@mui\/x-date-pickers(-pro|)(\/(.*)|)/;

const matchImport = (path: ASTPath<ImportDeclaration>) =>
(path.node.source.value?.toString() ?? '').match(PACKAGE_REGEXP);
export default function transformer(file: JsCodeShiftFileInfo, api: JsCodeShiftAPI, options: any) {
const j = api.jscodeshift;
const root = j(file.source);
Expand All @@ -63,38 +10,75 @@ export default function transformer(file: JsCodeShiftFileInfo, api: JsCodeShiftA
trailingComma: true,
};

const matchingImports = root.find(j.ImportDeclaration).filter((path) => !!matchImport(path));

// Rename the import specifiers
// - import { MonthPicker } from '@mui/x-date-pickers/MonthPicker'
// + import { MonthCalendar } from '@mui/x-date-pickers/MonthPicker'
matchingImports
.find(j.ImportSpecifier)
.filter((path) => VARIABLES.hasOwnProperty(path.node.imported.name))
.replaceWith((path) => j.importSpecifier(j.identifier(VARIABLES[path.node.imported.name])));

// Rename the nested import declarations
// - import {} from '@mui/x-date-pickers/MonthPicker'
// + import {} from '@mui/x-date-pickers/MonthCalendar'
matchingImports
.filter((path) => SUB_PACKAGES.hasOwnProperty(matchImport(path)?.[3] ?? ''))
.replaceWith((path) => {
const subPackage = matchImport(path)![3];
const importPath = path.node.source.value?.toString() ?? '';

return j.importDeclaration(
path.node.specifiers, // copy over the existing import specifiers
j.stringLiteral(importPath.replace(subPackage, SUB_PACKAGES[subPackage])), // Replace the source with our new source
);
});

// Rename the import usage
// - <CalendarPicker />
// + <DateCalendar />
root
.find(j.Identifier)
.filter((path) => VARIABLES.hasOwnProperty(path.node.name))
.replaceWith((path) => j.identifier(VARIABLES[path.node.name]));
renameImports({
j,
root,
packageNames: ['@mui/x-date-pickers', '@mui/x-date-pickers-pro'],
imports: [
{
oldEndpoint: 'CalendarPicker',
newEndpoint: 'DateCalendar',
importsMapping: {
CalendarPicker: 'DateCalendar',
CalendarPickerProps: 'DateCalendarProps',
CalendarPickerSlotsComponent: 'DateCalendarSlotsComponent',
CalendarPickerSlotsComponentsProps: 'DateCalendarSlotsComponentsProps',
CalendarPickerClasses: 'DateCalendarClasses',
CalendarPickerClassKey: 'DateCalendarClassKey',
calendarPickerClasses: 'dateCalendarClasses',
getCalendarPickerUtilityClass: 'getDateCalendarUtilityClass',
},
},
{
oldEndpoint: 'MonthPicker',
newEndpoint: 'MonthCalendar',
importsMapping: {
MonthPicker: 'MonthCalendar',
MonthPickerProps: 'MonthCalendarProps',
MonthPickerClasses: 'MonthCalendarClasses',
MonthPickerClassKey: 'MonthCalendarClassKey',
monthPickerClasses: 'monthCalendarClasses',
getMonthPickerUtilityClass: 'getMonthCalendarUtilityClass',
},
},
{
oldEndpoint: 'YearPicker',
newEndpoint: 'YearCalendar',
importsMapping: {
YearPicker: 'YearCalendar',
YearPickerProps: 'YearCalendarProps',
YearPickerClasses: 'YearCalendarClasses',
YearPickerClassKey: 'YearCalendarClassKey',
yearPickerClasses: 'yearCalendarClasses',
getYearPickerUtilityClass: 'getYearCalendarUtilityClass',
},
},
{
oldEndpoint: 'ClockPicker',
newEndpoint: 'TimeClock',
importsMapping: {
ClockPicker: 'TimeClock',
ClockPickerProps: 'TimeClockProps',
ClockPickerClasses: 'TimeClockClasses',
ClockPickerClassKey: 'TimeClockClassKey',
clockPickerClasses: 'timeClockClasses',
getClockPickerUtilityClass: 'getTimeClockUtilityClass',
},
},
{
oldEndpoint: 'CalendarPickerSkeleton',
newEndpoint: 'DayCalendarSkeleton',
importsMapping: {
CalendarPickerSkeleton: 'DayCalendarSkeleton',
CalendarPickerSkeletonProps: 'DayCalendarSkeletonProps',
CalendarPickerSkeletonClasses: 'DayCalendarSkeletonClasses',
CalendarPickerSkeletonClassKey: 'DayCalendarSkeletonClassKey',
calendarPickerSkeletonClasses: 'dayCalendarSkeletonClasses',
getCalendarPickerSkeletonUtilityClass: 'getDayCalendarSkeletonUtilityClass',
},
},
],
});

return root.toSource(printOptions);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('v6.0.0/pickers', () => {
const actualPath = `./actual-${testFile}.spec.tsx`;
const expectedPath = `./expected-${testFile}.spec.tsx`;

describe(`Community package (${testFile.replace(/-/g, ' ')})`, () => {
describe(`Package (${testFile.replace(/-/g, ' ')})`, () => {
it('transforms imports as needed', () => {
const actual = transform(
{ source: read(actualPath) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { TreeItem } from '@mui/x-tree-view/TreeItem';
function App() {
getTreeViewUtilityClass('root');

// prettier-ignore
return (
<TreeView>
<TreeItem nodeId="1" label="Item 1" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,16 @@ import {
SimpleTreeViewClassKey,
simpleTreeViewClasses,
getSimpleTreeViewUtilityClass,
} from '@mui/x-tree-view/TreeView';
} from '@mui/x-tree-view/SimpleTreeView';
import { TreeItem } from '@mui/x-tree-view/TreeItem';

function App() {
getSimpleTreeViewUtilityClass('root');

// prettier-ignore
return (
<SimpleTreeView>
(<SimpleTreeView>
<TreeItem nodeId="1" label="Item 1" />
</SimpleTreeView>
</SimpleTreeView>)
);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
/* eslint-disable no-restricted-imports */
// @ts-nocheck
import {
TreeView,
TreeViewProps,
TreeViewClasses,
TreeViewClassKey,
treeViewClasses,
getTreeViewUtilityClass,
SimpleTreeView,
SimpleTreeViewProps,
SimpleTreeViewClasses,
SimpleTreeViewClassKey,
simpleTreeViewClasses,
getSimpleTreeViewUtilityClass,
} from '@mui/x-tree-view';
Loading

0 comments on commit 185dd93

Please sign in to comment.