@itwin/appui-codemod
Version:
Codemod scripts for AppUI.
170 lines (146 loc) • 6.23 kB
text/typescript
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import type { ASTNode, Collection, JSCodeshift } from "jscodeshift";
import { toExpression, toExpressionName, useCallExpression } from "./CallExpression";
import { useImportDeclaration } from "./ImportDeclaration";
import { findRootIdentifiers, sortSpecifiers, useImportSpecifier } from "./ImportSpecifier";
import retainFirstComment from "./retainFirstComment";
import { usePlugin } from "./usePlugin";
declare module "jscodeshift/src/collection" {
interface Collection<N> extends GlobalMethods {
}
}
interface GlobalMethods {
contains(other: Collection): boolean;
concat(other: Collection): this;
rename(source: string, target: string): this;
}
function extensionsPlugin(j: JSCodeshift) {
const globalMethods: GlobalMethods = {
contains(this: Collection, other) {
const paths = this.paths();
const otherPaths = other.paths();
for (const path of paths) {
if (otherPaths.indexOf(path) >= 0)
return true;
}
return false;
},
concat(this: Collection, other) {
const paths = [...this.paths(), ...other.paths()];
const proto = Object.getPrototypeOf(this);
const collection = new proto.constructor(paths, this, this.getTypes());
return collection;
},
rename(this: Collection, sourceStr, targetStr) {
useImportDeclaration(j);
useImportSpecifier(j);
useCallExpression(j);
if (sourceStr === "" || targetStr === "")
throw new Error(`Expected non empty strings in rename("${sourceStr}", "${targetStr}")`);
const source = parseRename(sourceStr);
const target = parseRename(targetStr);
const sourceDeclarations = this.findImportDeclarations(source.module);
const sourceSpecifiers = sourceDeclarations.findSpecifiers(source.specifier);
const targetDeclarations = this.findImportDeclarations(target.module);
const targetSpecifiers = targetDeclarations.findSpecifiers(target.specifier);
if (sourceSpecifiers.size() + targetSpecifiers.size() === 0)
return this;
const sourceLocalSpecifierName = sourceSpecifiers.getLocalName() || source.specifier;
const sourceExpr = updateExpressionSpecifier(source.expr, sourceLocalSpecifierName);
retainFirstComment(j, this, () => {
let renamed = false;
const callExpressions = this.findCallExpressions(sourceExpr);
callExpressions.renameTo(target.expr);
if (callExpressions.size() > 0) {
renamed = true;
}
this.find(j.MemberExpression).filter((path) => {
const expression = toExpressionName(path.value);
return expression === sourceExpr;
}).forEach((path) => {
const callee = toExpression(j, target.expr);
path.replace(callee);
renamed = true;
});
if (source.isIdentifier) {
findRootIdentifiers(j, this, sourceExpr).forEach((path) => {
const callee = toExpression(j, target.expr);
path.replace(callee);
renamed = true;
});
}
this.findJSXElements(sourceExpr).forEach((path) => {
const identifier = path.value.openingElement.name as ASTNode;
if (identifier.type !== "JSXIdentifier") {
return;
}
identifier.name = target.expr;
const closingIdentifier = path.value.closingElement?.name;
if (closingIdentifier && closingIdentifier.type === "JSXIdentifier") {
closingIdentifier.name = target.expr;
}
renamed = true;
});
const targetDeclarations = this.findImportDeclarations(target.module);
const targetSpecifiers = targetDeclarations.findSpecifiers(target.specifier);
// Add new declaration.
if (targetDeclarations.size() === 0 && renamed) {
const newDeclaration = j.importDeclaration([
j.importSpecifier(j.identifier(target.specifier))
], j.literal(target.module));
sourceDeclarations.insertAfter(newDeclaration);
}
// Use existing target declaration.
if (targetDeclarations.size() > 0 && targetSpecifiers.size() === 0 && renamed) {
const targetDeclaration = targetDeclarations.nodes()[0];
targetDeclaration.specifiers = sortSpecifiers(j, [
...targetDeclaration.specifiers ?? [],
j.importSpecifier(j.identifier(target.specifier)),
]);
}
if (renamed && sourceLocalSpecifierName === target.specifier) {
sourceDeclarations.removeSpecifier(source.specifier);
} else if (!sourceSpecifiers.isUsed()) {
sourceDeclarations.removeSpecifier(source.specifier);
}
});
return this;
},
};
j.registerMethods(globalMethods);
}
export function useExtensions(j: JSCodeshift) {
usePlugin(j, extensionsPlugin);
}
function parseRename(rename: string) {
let substrings = rename.split(":");
if (substrings.length === 1) {
return {
module: "",
...parseExpression(rename)
};
}
const module = substrings[0];
const expr = substrings[1];
return {
module,
...parseExpression(expr),
};
}
function parseExpression(expr: string) {
const substrings = expr.split(".");
let specifier = substrings[0];
const isIndexedAccessType = expr.includes("[") && expr.includes("]");
const isIdentifier = substrings.length === 1;
if (isIndexedAccessType) {
specifier = expr.split("[")[0];
}
return { specifier, expr, isIndexedAccessType, isIdentifier };
}
function updateExpressionSpecifier(expr: string, specifier: string) {
const substrings = expr.split(".");
return [specifier, ...substrings.slice(1)].join(".");
}