@itwin/appui-codemod
Version:
Codemod scripts for AppUI.
565 lines (499 loc) • 24.2 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 { FileInfo, API, Options, ASTPath, JSCodeshift, JSXElement, ObjectProperty, SpreadProperty, Collection, JSXIdentifier, ObjectExpression, Property } from "jscodeshift";
import { ConfigExpression, jsxElementToUniFormat, buildProperty, isSpecifiedConfigExpression, isExpressionKind, ExpressionKind, uniFormatToObjectExpression, concatExpressions, getObjectProperty, handleConditionalExpression, takeFirstArrayExpressionElement } from "../utils/TransformUtils";
import { isSpecifiedJSXElement } from "../utils/typeGuards";
import { useImportDeclaration } from "../utils/ImportDeclaration";
import { useImportSpecifier } from "../utils/ImportSpecifier";
import { useObjectExpression, ObjectExpressionCollection } from "../utils/ObjectExpression";
import { transformAbstractWidget } from "./widget";
export default function transformer(file: FileInfo, api: API, options: Options) {
const j = api.jscodeshift;
useImportDeclaration(j);
useImportSpecifier(j);
const root = j(file.source);
const appuiReactImportDec = root.findImportDeclarations("@itwin/appui-react");
const widgetName = appuiReactImportDec.findSpecifiers("Widget").getLocalName();
widgetName !== "" && root.findJSXElements(widgetName).forEach((widget) => {
const widgetConfig = transformWidget(j, widget);
widget.replace(widgetConfig);
});
const zoneName = appuiReactImportDec.findSpecifiers("Zone").getLocalName();
zoneName !== "" && root.findJSXElements(zoneName).forEach((zone) => {
const zoneConfig = transformZone(j, zone);
zone.replace(zoneConfig);
});
const stagePanelName = appuiReactImportDec.findSpecifiers("StagePanel").getLocalName();
stagePanelName !== "" && root.findJSXElements(stagePanelName).forEach((stagePanel) => {
const stagePanelConfig = transformStagePanel(j, stagePanel);
stagePanel.replace(stagePanelConfig);
});
const frontstageName = appuiReactImportDec.findSpecifiers("Frontstage").getLocalName();
frontstageName !== "" && root.findJSXElements(frontstageName).forEach((frontstage) => {
const frontstageConfig = transformFrontstage(j, frontstage);
frontstage.replace(frontstageConfig);
});
appuiReactImportDec
.removeSpecifier("Widget")
.removeSpecifier("Zone")
.removeSpecifier("StagePanel")
.removeSpecifier("Frontstage");
return root.toSource(options.printOptions);
}
export function transformFrontstage(j: JSCodeshift, frontstage: ASTPath<JSXElement>): ConfigExpression { // TODO: registerMethod
const uni = jsxElementToUniFormat(j, frontstage);
const { name: configName, attrMap, spreadAttr } = uni;
const frontstageProps: (ObjectProperty | SpreadProperty)[] = [];
const handledExpressions = new Set(["key", "applicationData", "defaultTool", "defaultContentId", "isInFooterMode", "isIModelIndependent", "runtimeProps"]);
const appuiReactImportDec = j(frontstage).closest(j.File).findImportDeclarations("@itwin/appui-react");
const zoneName = appuiReactImportDec.findSpecifiers("Zone").getLocalName();
const stagePanelName = appuiReactImportDec.findSpecifiers("StagePanel").getLocalName();
const idExpr = attrMap.getAttributeExpression("id") ?? attrMap.getAttributeExpression("key");
if (idExpr) {
const id = buildProperty(j, "id", idExpr);
frontstageProps.push(id);
handledExpressions.add("id");
}
else {
// TODO: Handle missing mandatory property
}
const contentGroupExpr = attrMap.getAttributeExpression("contentGroup");
if (contentGroupExpr) {
const contentGroup = buildProperty(j, "contentGroup", contentGroupExpr);
frontstageProps.push(contentGroup);
handledExpressions.add("contentGroup");
}
else {
// TODO: Handle missing mandatory property
}
const usageExpr = attrMap.getAttributeExpression("usage");
if (usageExpr) {
const usage = buildProperty(j, "usage", usageExpr);
frontstageProps.push(usage);
handledExpressions.add("usage");
}
const versionExpr = attrMap.getAttributeExpression("version");
if (versionExpr) {
const version = buildProperty(j, "version", versionExpr);
frontstageProps.push(version);
handledExpressions.add("version");
}
else {
// TODO: Handle missing mandatory property
}
const contentManipulationExpr = attrMap.getAttributeExpression("contentManipulationTools") ?? attrMap.getAttributeExpression("topLeft");
if (contentManipulationExpr) {
const modifiedContentManipulationExpr = handleToolWidget(j, contentManipulationExpr);
const contentManipulation = buildProperty(j, "contentManipulation", modifiedContentManipulationExpr);
frontstageProps.push(contentManipulation);
handledExpressions.add("contentManipulationTools");
handledExpressions.add("topLeft");
}
const toolSettingsExpr = attrMap.getAttributeExpression("toolSettings") ?? attrMap.getAttributeExpression("topCenter");
if (toolSettingsExpr) {
const modifiedToolSettingsExpr = handleToolWidget(j, toolSettingsExpr);
const toolSettings = buildProperty(j, "toolSettings", modifiedToolSettingsExpr);
frontstageProps.push(toolSettings);
handledExpressions.add("toolSettings");
handledExpressions.add("topCenter");
}
const viewNavigationExpr = attrMap.getAttributeExpression("viewNavigationTools") ?? attrMap.getAttributeExpression("topRight");
if (viewNavigationExpr) {
const modifiedViewNavigationExpr = handleToolWidget(j, viewNavigationExpr);
const viewNavigation = buildProperty(j, "viewNavigation", modifiedViewNavigationExpr);
frontstageProps.push(viewNavigation);
handledExpressions.add("viewNavigationTools");
handledExpressions.add("topRight");
}
const statusBarExpr = attrMap.getAttributeExpression("statusBar") ?? attrMap.getAttributeExpression("bottomCenter");
if (statusBarExpr) {
const modifiedStatusBarExpr = handleToolWidget(j, statusBarExpr);
const statusBar = buildProperty(j, "statusBar", modifiedStatusBarExpr);
frontstageProps.push(statusBar);
handledExpressions.add("statusBar");
handledExpressions.add("bottomCenter");
}
let topPanelExpr = attrMap.getAttributeExpression("topPanel");
let topPanelEnd = attrMap.getAttributeExpression("topMostPanel");
if (topPanelExpr || topPanelEnd) {
if (topPanelExpr) {
topPanelExpr = handleSideStagePanel(j, topPanelExpr, stagePanelName);
if (topPanelExpr)
handledExpressions.add("topPanel");
}
else {
topPanelExpr = createEmptyConfig(j, j.jsxIdentifier("StagePanel"));
}
if (topPanelExpr && topPanelEnd) {
topPanelEnd = handleSideStagePanel(j, topPanelEnd, stagePanelName);
if (topPanelEnd && isSpecifiedConfigExpression(topPanelEnd, "StagePanel")) {
handledExpressions.add("topMostPanel");
const start = getStagePanelSectionProperty(j, topPanelEnd, "start")?.value;
const end = getStagePanelSectionProperty(j, topPanelEnd, "end")?.value;
let successfulAppend: boolean = true;
if (start && isExpressionKind(j, start) && isSpecifiedConfigExpression(topPanelExpr, "StagePanel"))
successfulAppend = appendStagePanelSection(j, topPanelExpr, "end", start) && successfulAppend ? true : false;
if (end && isExpressionKind(j, end) && isSpecifiedConfigExpression(topPanelExpr, "StagePanel"))
successfulAppend = appendStagePanelSection(j, topPanelExpr, "end", end) && successfulAppend ? true : false;
if ((start || end) && successfulAppend)
handledExpressions.add("topMostPanel");
}
}
if (topPanelExpr) {
const topPanel = buildProperty(j, "topPanel", topPanelExpr);
frontstageProps.push(topPanel);
}
}
let leftPanelExpr = attrMap.getAttributeExpression("leftPanel");
let leftPanelStart = attrMap.getAttributeExpression("centerLeft");
let leftPanelEnd = attrMap.getAttributeExpression("bottomLeft");
if (leftPanelExpr || leftPanelStart || leftPanelEnd) {
if (leftPanelExpr) {
leftPanelExpr = handleSideStagePanel(j, leftPanelExpr, stagePanelName);
if (leftPanelExpr)
handledExpressions.add("leftPanel");
}
else {
leftPanelExpr = createEmptyConfig(j, j.jsxIdentifier("StagePanel"));
}
leftPanelStart = leftPanelStart ? handleSideStagePanelZone(j, leftPanelStart, zoneName) : undefined;
let successfulAppend: boolean = true;
if (leftPanelExpr && leftPanelStart)
successfulAppend = appendStagePanelSection(j, leftPanelExpr as ConfigExpression, "start", leftPanelStart) && successfulAppend ? true : false;
if (leftPanelExpr && successfulAppend)
handledExpressions.add("centerLeft");
successfulAppend = true;
leftPanelEnd = leftPanelEnd ? handleSideStagePanelZone(j, leftPanelEnd, zoneName) : undefined;
if (leftPanelExpr && leftPanelEnd)
successfulAppend = appendStagePanelSection(j, leftPanelExpr as ConfigExpression, "end", leftPanelEnd) && successfulAppend ? true : false;
if (leftPanelExpr && successfulAppend)
handledExpressions.add("bottomLeft");
if (leftPanelExpr) {
const leftPanel = buildProperty(j, "leftPanel", leftPanelExpr);
frontstageProps.push(leftPanel);
}
}
let rightPanelExpr = attrMap.getAttributeExpression("rightPanel");
let rightPanelStart = attrMap.getAttributeExpression("centerRight");
let rightPanelEnd = attrMap.getAttributeExpression("bottomRight");
if (rightPanelExpr || rightPanelStart || rightPanelEnd) {
if (rightPanelExpr) {
rightPanelExpr = handleSideStagePanel(j, rightPanelExpr, stagePanelName);
if (rightPanelExpr)
handledExpressions.add("rightPanel");
}
else {
rightPanelExpr = createEmptyConfig(j, j.jsxIdentifier("StagePanel"));
}
rightPanelStart = rightPanelStart ? handleSideStagePanelZone(j, rightPanelStart, zoneName) : undefined;
let successfulAppend: boolean = true;
if (rightPanelExpr && rightPanelStart)
successfulAppend = appendStagePanelSection(j, rightPanelExpr as ConfigExpression, "start", rightPanelStart) && successfulAppend ? true : false;
if (rightPanelExpr && successfulAppend)
handledExpressions.add("centerRight");
successfulAppend = true;
rightPanelEnd = rightPanelEnd ? handleSideStagePanelZone(j, rightPanelEnd, zoneName) : undefined;
if (rightPanelExpr && rightPanelEnd)
successfulAppend = appendStagePanelSection(j, rightPanelExpr as ConfigExpression, "end", rightPanelEnd) && successfulAppend ? true : false;
if (rightPanelExpr && successfulAppend)
handledExpressions.add("bottomRight");
if (rightPanelExpr) {
const rightPanel = buildProperty(j, "rightPanel", rightPanelExpr);
frontstageProps.push(rightPanel);
}
}
let bottomPanelExpr = attrMap.getAttributeExpression("bottomPanel");
let bottomPanelEnd = attrMap.getAttributeExpression("bottomMostPanel");
if (bottomPanelExpr || bottomPanelEnd) {
if (bottomPanelExpr) {
bottomPanelExpr = handleSideStagePanel(j, bottomPanelExpr, stagePanelName);
if (bottomPanelExpr)
handledExpressions.add("bottomPanel");
}
else {
bottomPanelExpr = createEmptyConfig(j, j.jsxIdentifier("StagePanel"));
}
if (bottomPanelExpr && bottomPanelEnd) {
bottomPanelEnd = handleSideStagePanel(j, bottomPanelEnd, stagePanelName);
if (bottomPanelEnd && isSpecifiedConfigExpression(bottomPanelEnd, "StagePanel")) {
const start = getStagePanelSectionProperty(j, bottomPanelEnd, "start")?.value;
const end = getStagePanelSectionProperty(j, bottomPanelEnd, "end")?.value;
let successfulAppend: boolean = true;
if (start && isExpressionKind(j, start) && isSpecifiedConfigExpression(bottomPanelExpr, "StagePanel"))
successfulAppend = appendStagePanelSection(j, bottomPanelExpr, "end", start) && successfulAppend ? true : false;
if (end && isExpressionKind(j, end) && isSpecifiedConfigExpression(bottomPanelExpr, "StagePanel"))
successfulAppend = appendStagePanelSection(j, bottomPanelExpr, "end", end) && successfulAppend ? true : false;
if ((start || end) && successfulAppend)
handledExpressions.add("bottomMostPanel");
}
}
if (bottomPanelExpr) {
const bottomPanel = buildProperty(j, "bottomPanel", bottomPanelExpr);
frontstageProps.push(bottomPanel);
}
}
const styleExpr = attrMap.getAttributeExpression("style");
if (styleExpr) {
const style = buildProperty(j, "style", styleExpr);
frontstageProps.push(style);
handledExpressions.add("style");
}
const itemIdExpr = attrMap.getAttributeExpression("itemId");
if (itemIdExpr) {
const itemId = buildProperty(j, "itemId", itemIdExpr);
frontstageProps.push(itemId);
handledExpressions.add("itemId");
}
const classNameExpr = attrMap.getAttributeExpression("className");
if (classNameExpr) {
const className = buildProperty(j, "className", classNameExpr);
frontstageProps.push(className);
handledExpressions.add("className");
}
spreadAttr.forEach((expr) => {
const spreadProperty = buildProperty(j, null, expr);
frontstageProps.push(spreadProperty);
});
attrMap.forEach((value, key) => {
if (handledExpressions.has(key) === false && value && value !== "UnhandledExpression") {
const unhandledProp = j.objectProperty(j.identifier(key), value);
frontstageProps.push(unhandledProp);
}
});
return { ...j.objectExpression(frontstageProps), configName };
}
export function transformStagePanel(j: JSCodeshift, stagePanel: ASTPath<JSXElement>): ConfigExpression { // TODO: registerMethod
const { name: configName, attrMap, spreadAttr } = jsxElementToUniFormat(j, stagePanel);
const stagePanelProps: (ObjectProperty | SpreadProperty)[] = [];
const handledExpressions = new Set(["key", "allowedZones", "applicationData", "runtimeProps", "isInFooterMode", "isIModelIndependent", "runtimeProps"]);
const defaultStateExpr = attrMap.getAttributeExpression("defaultState");
if (defaultStateExpr) {
const defaultState = buildProperty(j, "defaultState", defaultStateExpr);
stagePanelProps.push(defaultState);
handledExpressions.add("defaultState");
}
const maxSizeExpr = attrMap.getAttributeExpression("maxSize");
if (maxSizeExpr) {
const maxSize = buildProperty(j, "maxSize", maxSizeExpr);
stagePanelProps.push(maxSize);
handledExpressions.add("maxSize");
}
const minSizeExpr = attrMap.getAttributeExpression("minSize");
if (minSizeExpr) {
const minSize = buildProperty(j, "minSize", minSizeExpr);
stagePanelProps.push(minSize);
handledExpressions.add("minSize");
}
const pinnedExpr = attrMap.getAttributeExpression("pinned");
if (pinnedExpr) {
const pinned = buildProperty(j, "pinned", pinnedExpr);
stagePanelProps.push(pinned);
handledExpressions.add("pinned");
}
const resizableExpr = attrMap.getAttributeExpression("resizable");
if (resizableExpr) {
const resizable = buildProperty(j, "resizable", resizableExpr);
stagePanelProps.push(resizable);
handledExpressions.add("resizable");
}
const sizeExpr = attrMap.getAttributeExpression("size");
if (sizeExpr) {
const size = buildProperty(j, "size", sizeExpr);
stagePanelProps.push(size);
handledExpressions.add("size");
}
let panelZonesExpr = attrMap.getAttributeExpression("panelZones");
let widgetsExpr = attrMap.getAttributeExpression("widgets");
if (panelZonesExpr || widgetsExpr) {
let startExpr = j.arrayExpression([]);
let endExpr = j.arrayExpression([]);
if (panelZonesExpr?.type === "ObjectExpression") {
const [start, end] = extractSectionsFromPanelZones(j, panelZonesExpr);
startExpr = concatExpressions(j, startExpr, start);
endExpr = concatExpressions(j, endExpr, end);
handledExpressions.add("panelZones");
}
if (widgetsExpr) {
startExpr = concatExpressions(j, startExpr, widgetsExpr);
handledExpressions.add("widgets");
}
const sectionsObjectProps: ObjectProperty[] = [];
if (startExpr.elements.length > 0)
sectionsObjectProps.push(j.objectProperty(j.identifier("start"), startExpr));
if (endExpr.elements.length > 0)
sectionsObjectProps.push(j.objectProperty(j.identifier("end"), endExpr));
if (sectionsObjectProps.length > 0) {
const sectionsObject = j.objectExpression(sectionsObjectProps);
const sections = buildProperty(j, "sections", sectionsObject);
stagePanelProps.push(sections);
}
}
spreadAttr.forEach((expr) => {
const spreadProperty = buildProperty(j, null, expr);
stagePanelProps.push(spreadProperty);
});
attrMap.forEach((value, key) => {
if (handledExpressions.has(key) === false && value && value !== "UnhandledExpression") {
const unhandledProp = j.objectProperty(j.identifier(key), value);
stagePanelProps.push(unhandledProp);
}
});
return { ...j.objectExpression(stagePanelProps), configName };
}
export function transformZone(j: JSCodeshift, zone: ASTPath<JSXElement>): ExpressionKind {
const { attrMap, spreadAttr } = jsxElementToUniFormat(j, zone);
const widgetsExpr = attrMap.getAttributeExpression("widgets");
if (widgetsExpr)
return widgetsExpr;
if (spreadAttr.length === 1) {
return spreadAttr[0];
}
else if (spreadAttr.length > 1) {
// TODO: log warning
return spreadAttr[0];
}
return j.identifier("undefined");
}
export function transformWidget(j: JSCodeshift, widget: ASTPath<JSXElement>): ConfigExpression { // TODO: registerMethod
const uni = jsxElementToUniFormat(j, widget);
const widgetObj = uniFormatToObjectExpression(j, uni);
replaceWidgetObjectExpression(j, j(widgetObj));
return { ...widgetObj, configName: uni.name };
}
export function handleSideStagePanel(j: JSCodeshift, expression: ExpressionKind, panelName: string): ConfigExpression | undefined {
if (isSpecifiedJSXElement(expression, panelName)) {
expression = transformFrontstage(j, j(expression).getAST()[0]);
}
if (isSpecifiedConfigExpression(expression, "StagePanel")) {
return expression;
}
return undefined;
}
export function handleSideStagePanelZone(j: JSCodeshift, expression: ExpressionKind, panelZoneName: string): ExpressionKind | undefined {
if (isSpecifiedJSXElement(expression, panelZoneName)) {
expression = transformZone(j, j(expression).getAST()[0]);
}
if (expression.type === "Identifier" && expression.name === "undefined") {
return undefined;
}
return expression;
}
export function createEmptyConfig(j: JSCodeshift, configName: JSXIdentifier): ConfigExpression {
return { ...j.objectExpression([]), configName };
}
export function handleToolWidget(j: JSCodeshift, expression: ExpressionKind): ExpressionKind {
function handleExpression(expr: ExpressionKind): ExpressionKind {
let newExpr: ExpressionKind | undefined = undefined;
if (!newExpr && expr.type === "ConditionalExpression")
newExpr = handleConditionalExpression(j, expr, handleExpression);
if (!newExpr && expr.type === "ArrayExpression")
newExpr = takeFirstArrayExpressionElement(j, expr);
if (!newExpr) {
// TODO: log warning
return expr;
}
return newExpr;
}
return handleExpression(expression);
}
export function extractSectionsFromPanelZones(j: JSCodeshift, expression: ObjectExpression) {
const result: [ExpressionKind | undefined, ExpressionKind | undefined] = [undefined, undefined]
const start = getObjectProperty(j, expression, "start")?.value;
if (start && start.type === "ObjectExpression") {
const startWidgets = getObjectProperty(j, start, "widgets")?.value;
if (startWidgets && isExpressionKind(j, startWidgets))
result[0] = startWidgets;
}
else if (start && isExpressionKind(j, start)) {
result[0] = j.memberExpression(start, j.identifier("widgets"));
}
const middle = getObjectProperty(j, expression, "middle")?.value;
if (middle && middle.type === "ObjectExpression") {
const middleWidgets = getObjectProperty(j, middle, "widgets")?.value;
if (middleWidgets && isExpressionKind(j, middleWidgets)) {
if (result[0])
result[0] = concatExpressions(j, result[0], middleWidgets);
else
result[0] = middleWidgets;
}
}
else if (middle && isExpressionKind(j, middle)) {
const middleMemberExpr = j.memberExpression(middle, j.identifier("widgets"))
if (result[0])
result[0] = concatExpressions(j, result[0], middleMemberExpr);
else
result[0] = middleMemberExpr;
}
const end = getObjectProperty(j, expression, "end")?.value;
if (end && end.type === "ObjectExpression") {
const endWidgets = getObjectProperty(j, end, "widgets")?.value;
if (endWidgets && isExpressionKind(j, endWidgets))
result[1] = endWidgets;
}
else if (end && isExpressionKind(j, end)) {
result[1] = j.memberExpression(end, j.identifier("widgets"));
}
return result;
}
export function appendStagePanelSection(j: JSCodeshift, stagePanel: ConfigExpression, sectionToAppend: "start" | "end", exprToAppend: ExpressionKind): "Success" | undefined {
let sections = getObjectProperty(j, stagePanel, "sections");
if (!sections) {
sections = j.objectProperty(j.identifier("sections"), j.objectExpression([]));
stagePanel.properties.push(sections);
}
if (sections.value.type !== "ObjectExpression") {
// TODO: log warning
return undefined;
}
let section = getObjectProperty(j, sections.value, sectionToAppend);
if (!section) {
section = j.objectProperty(j.identifier(sectionToAppend), j.arrayExpression([]));
sections.value.properties.push(section);
}
if (!isExpressionKind(j, section.value)) {
// TODO: log warning
return undefined;
}
section.value = concatExpressions(j, section.value, exprToAppend);
return "Success";
}
export function getStagePanelSectionProperty(j: JSCodeshift, stagePanel: ConfigExpression, sectionToGet: "start" | "end"): ObjectProperty | Property | undefined {
const sections = getObjectProperty(j, stagePanel, "sections");
if (!sections)
return undefined;
if (sections.value.type !== "ObjectExpression") {
// TODO: log warning
return undefined;
}
const section = getObjectProperty(j, sections.value, sectionToGet);
if (!section)
return undefined;
if (!isExpressionKind(j, section.value)) {
// TODO: log warning
return undefined;
}
return section;
}
export function replaceWidgetObjectExpression(j: JSCodeshift, widget: Collection<ObjectExpression>) {
useObjectExpression(j);
transformAbstractWidget(j, widget as ObjectExpressionCollection)
.removeProperty("key", (_path, property) => {
const idProp = getObjectProperty(j, _path.node, "id");
if (idProp === undefined && property.type === "ObjectProperty") {
const newIdProp = j.objectProperty(j.identifier("id"), property.value);
_path.node.properties.push(newIdProp);
}
})
.removeProperty("isFreeform")
.removeProperty("isToolSettings")
.removeProperty("isStatusBar")
.removeProperty("fillZone")
.removeProperty("syncEventIds")
.removeProperty("stateFunc")
.renameProperty("iconSpec", "icon")
.renameProperty("isFloatingStateSupported", "canFloat")
.renameProperty("badgeType", "badge")
.renameProperty("element", "content");
}