@itwin/appui-codemod
Version:
Codemod scripts for AppUI.
161 lines (145 loc) • 6.39 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 { ObjectExpression, ObjectProperty, JSCodeshift, ASTPath, JSXElement, JSXIdentifier, Identifier, JSXMemberExpression, MemberExpression, SpreadProperty, ASTNode, RestElement, Property, Collection, ExpressionStatement, ArrayExpression, ConditionalExpression } from "jscodeshift";
export type ExpressionKind = ExpressionStatement["expression"];
export type PatternKind = RestElement["argument"];
export interface ConfigExpression extends ObjectExpression {
configName: JSXIdentifier | JSXMemberExpression; // TODO: figure out a better way to store
}
export class AttributeMap extends Map<string, ExpressionKind | "UnhandledExpression"> {
public getAttributeExpression(name: string, errorHandle?: () => void): ExpressionKind | undefined {
const attrExpr = this.get(name);
if (attrExpr === "UnhandledExpression" || attrExpr === undefined) {
errorHandle?.();
return undefined;
}
return attrExpr;
}
}
export interface UniFormat {
name: JSXIdentifier | JSXMemberExpression;
attrMap: AttributeMap;
spreadAttr: ExpressionKind[];
}
export function isExpressionKind(j: JSCodeshift, source: ASTNode): source is ExpressionKind {
return j(source).isOfType(j.Expression);
}
export function isSpecifiedConfigExpression(source: ASTNode, name: string): source is ConfigExpression {
return source.type === "ObjectExpression" && "configName" in source && getName((source as ConfigExpression).configName) === name;
}
export function getName(name: JSXIdentifier | Identifier | JSXMemberExpression | MemberExpression): string | undefined {
if (name.type === "JSXIdentifier" || name.type === "Identifier")
return name.name;
if (name.property.type === "JSXIdentifier" || name.property.type === "Identifier")
return name.property.name;
return undefined;
}
export function buildProperty(j: JSCodeshift, name: string | null, expression: ExpressionKind): ObjectProperty | SpreadProperty {
if (name !== null) {
const identifier = j.identifier(name);
if (expression.type === "Identifier" && expression.name === name) {
const objProperty = j.objectProperty(identifier, identifier);
objProperty.shorthand = true; // TODO: write tests for this functionality
return objProperty;
}
return j.objectProperty(identifier, expression);
}
return j.spreadProperty(expression);
}
export function jsxElementToUniFormat(j: JSCodeshift, element: ASTPath<JSXElement>): UniFormat {
const name = element.node.openingElement.name as JSXIdentifier | JSXMemberExpression;
const attrMap = new AttributeMap();
const spreadAttr: ExpressionKind[] = [];
element.node.openingElement.attributes?.forEach((attr) => {
if (attr.type === "JSXAttribute") {
if (attr.name.type !== "JSXIdentifier")
return;
const key = attr.name.name;
const value = attr.value;
if (!value)
attrMap.set(key, j.identifier(key));
else if (value.type === "JSXExpressionContainer")
attrMap.set(key, value.expression.type === "JSXEmptyExpression" ? "UnhandledExpression" : value.expression);
else
attrMap.set(key, value);
}
else {
spreadAttr.push(attr.argument);
}
});
return {
name,
attrMap,
spreadAttr,
};
}
export function uniFormatToObjectExpression(j: JSCodeshift, uni: UniFormat) {
const props: (ObjectProperty | SpreadProperty)[] = [];
uni.attrMap.forEach((value, key) => {
if (value !== "UnhandledExpression")
props.push(buildProperty(j, key, value));
});
uni.spreadAttr.forEach((expr) => {
const spreadProperty = buildProperty(j, null, expr);
props.push(spreadProperty);
});
return j.objectExpression(props);
}
export function getObjectProperty(j: JSCodeshift, expression: ObjectExpression, propName: string): ObjectProperty | Property | undefined {
let spreadWarn = false;
for (const prop of expression.properties) {
if (prop.type === "Property" || prop.type === "ObjectProperty") {
if (("name" in prop.key && prop.key.name === propName) || (prop.key.type === "Literal" && prop.key.value === propName)) {
return prop;
}
}
else if (prop.type === "SpreadElement" || prop.type === "SpreadProperty") {
spreadWarn = true;
}
}
if (spreadWarn) {
// TODO: log warning
}
return undefined;
}
export function takeFirstArrayExpressionElement(j: JSCodeshift, expression: ArrayExpression): ExpressionKind | undefined {
const elements = expression.elements;
if (elements.length === 0)
return j.identifier("undefined");
if (elements.length > 1) {
// TODO: log warning
}
const element = elements[0];
if (element === null) {
// TODO: log warning
return undefined;
}
if (element.type === "SpreadElement") {
// TODO: log warning
return j.memberExpression(element.argument, j.literal(0), true);
}
if (element.type === "RestElement") {
// TODO: log warning
return undefined;
}
return element;
}
export function handleConditionalExpression(j: JSCodeshift, expression: ConditionalExpression, handle: (expr: ExpressionKind) => ExpressionKind): ConditionalExpression {
const consequent = handle(expression.consequent);
const alternate = handle(expression.alternate);
return j.conditionalExpression(expression.test, consequent, alternate);
}
export function concatExpressions(j: JSCodeshift, expression1: ExpressionKind | undefined, expression2: ExpressionKind | undefined): ArrayExpression {
const result = j.arrayExpression([]);
if (expression1?.type === "ArrayExpression")
result.elements.push(...expression1.elements);
else if (expression1)
result.elements.push(j.spreadElement(expression1));
if (expression2?.type === "ArrayExpression")
result.elements.push(...expression2.elements);
else if (expression2)
result.elements.push(j.spreadElement(expression2));
return result;
}