@eclipse-scout/migrate
Version:
TypeScript migration module
387 lines (333 loc) • 14.4 kB
JavaScript
/*
* Copyright (c) 2010, 2023 BSI Business Systems Integration AG
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
// noinspection SpellCheckingInspection
import fs from 'fs';
import jscodeshift from 'jscodeshift';
import {crlfToLf, defaultRecastOptions, findParentPath, insertMissingImportsForTypes, mapType, removeEmptyLinesBetweenImports} from './common.js';
const j = jscodeshift.withParser('ts');
/**
* @type import('ts-migrate-server').Plugin<unknown>
*/
const widgetColumnMapPlugin = {
name: 'widget-column-map-plugin',
async run({text, fileName, options}) {
let className = fileName.substring(fileName.lastIndexOf('/') + 1, fileName.lastIndexOf('.'));
if (!className || !className.endsWith('Model')) {
return text;
}
let root = j(text);
let widgets = new Map(),
tables = new Map();
// parse model and find all objects containing an id and objectType property
// noinspection JSCheckFunctionSignatures
root.find(j.ExportDefaultDeclaration)
.find(j.ArrowFunctionExpression)
.find(j.ObjectExpression, node => findObjectProperty(node, 'id') && findObjectProperty(node, 'objectType'))
.forEach(path => {
let node = path.node,
idAndObjectType = getIdAndObjectType(node),
objectType = idAndObjectType.objectType;
if (isWidget(objectType)) {
// remember id and objectType of all widget nodes
widgets.set(node, idAndObjectType);
}
if (isColumn(objectType)) {
// collect all column infos for one table
let tablePath = findParentTablePath(path);
if (!tablePath) {
return;
}
let tableInfo = tables.get(tablePath.node);
if (!tableInfo) {
let tableFieldPath = findParentTableFieldPath(tablePath);
tableInfo = {
// create a table class name from the id of the table or the id of the tableField
tableClassName: createTableClassName(getId(tablePath.node), tableFieldPath && getId(tableFieldPath.node)),
columns: new Map()
};
tables.set(tablePath.node, tableInfo);
}
// remember id and objectType of all column nodes
let columns = tableInfo.columns;
columns.set(node, idAndObjectType);
}
});
if (widgets.size > 0) { // only check size of widgets, if there are entries in tables then there are entries in widgets
let body = root.get().node.program.body;
// get/create widgetMap
let widgetName = className.substring(0, className.lastIndexOf('Model')),
widgetMapName = `${widgetName}WidgetMap`,
widgetMapType = getOrCreateExportedType(widgetMapName, root, body),
widgetMapMembers = getMembers(widgetMapType),
widgetMapProperties = [];
// create a property for every entry of widgets
widgets.forEach(({id, objectType}, node) => {
let tableInfo = tables.get(node);
if (tableInfo) {
// add specific table class if available, will be created later on
objectType = tableInfo.tableClassName;
}
widgetMapProperties.push(createMapProperty(id, objectType));
});
// set/replace properties of widgetMap
widgetMapMembers.splice(0, widgetMapMembers.length, ...widgetMapProperties);
// create table class and columnMap for every table
tables.forEach((tableInfo, node) => {
// get/create columnMap
let columnMapName = `${tableInfo.tableClassName}ColumnMap`,
columnMapType = getOrCreateExportedType(columnMapName, root, body),
columnMapMembers = getMembers(columnMapType),
columnMapProperties = [];
// create a property for every entry of tableInfo.columns
tableInfo.columns.forEach(({id, objectType}) => {
columnMapProperties.push(createMapProperty(id, objectType));
});
// set/replace properties of columnMap
columnMapMembers.splice(0, columnMapMembers.length, ...columnMapProperties);
// get/create tableClass
let tableClassName = tableInfo.tableClassName,
tableClass = getOrCreateExportedClass(tableClassName, root, body),
tableMembers = tableClass.body.body,
tableSuperClass = widgets.get(node).objectType,
columnMapProperty = createClassProperty('columnMap', columnMapName);
// set superClass to objectType from model
tableClass.superClass = j.identifier(tableSuperClass);
// declare columnMap property
columnMapProperty.declare = true;
tableMembers.splice(0, tableMembers.length, columnMapProperty);
});
let mainWidgetFileNameWithoutFileExtension = fileName.substring(0, fileName.lastIndexOf('Model')),
mainWidgetFileName, mainWidgetRoot;
// look for main widget as .js
mainWidgetFileName = mainWidgetFileNameWithoutFileExtension + '.js';
mainWidgetRoot = getMainWidgetRoot(mainWidgetFileName);
// look for main widget as .ts
if (!mainWidgetRoot) {
mainWidgetFileName = mainWidgetFileNameWithoutFileExtension + '.ts';
mainWidgetRoot = getMainWidgetRoot(mainWidgetFileName);
}
if (mainWidgetRoot) {
if (mainWidgetFileName.endsWith('.js')) {
// add widgetMap property with @type in .js case
// noinspection JSCheckFunctionSignatures
mainWidgetRoot
.find(j.ClassDeclaration, {id: {name: widgetName}})
.find(j.ClassMethod, {kind: 'constructor'})
.forEach(/** NodePath<namedTypes.ClassMethod, namedTypes.ClassMethod> */path => {
let node = path.node,
constructorMembers = node.body.body,
widgetMapAssignmentExpression = findConstructorAssignmentExpression(constructorMembers, 'widgetMap');
if (!widgetMapAssignmentExpression) {
// create an assignment expression after the super call
let superCall = findConstructorSuperCall(constructorMembers),
superCallIndex = constructorMembers.indexOf(superCall) + 1;
widgetMapAssignmentExpression = createAssignmentExpressionWithNull('widgetMap');
constructorMembers.splice(superCallIndex, 0, widgetMapAssignmentExpression);
}
// add type comment
widgetMapAssignmentExpression.comments = [createJsDocTypeComment(widgetMapName)];
});
}
if (mainWidgetFileName.endsWith('.ts')) {
// declare widgetMap property in .ts case
// noinspection JSCheckFunctionSignatures
mainWidgetRoot
.find(j.ClassDeclaration, {id: {name: widgetName}})
.forEach(/** NodePath<namedTypes.ClassDeclaration, namedTypes.ClassDeclaration> */path => {
let node = path.node,
classMembers = node.body.body,
widgetMapProperty = findClassProperty(classMembers, 'widgetMap');
if (widgetMapProperty) {
widgetMapProperty.typeAnnotation = createTypeAnnotation(widgetMapName);
} else {
widgetMapProperty = createClassProperty('widgetMap', widgetMapName);
classMembers.splice(0, 0, widgetMapProperty);
}
widgetMapProperty.declare = true;
// remove widgetMap assignment from constructor
let classConstructor = findConstructor(classMembers),
constructorMembers = classConstructor ? classConstructor.body.body : [],
widgetMapAssignmentExpression = classConstructor ? findConstructorAssignmentExpression(constructorMembers, 'widgetMap') : null;
if (widgetMapAssignmentExpression) {
constructorMembers.splice(constructorMembers.indexOf(widgetMapAssignmentExpression), 1);
}
});
// insert missing imports
insertMissingImportsForTypes(j, mainWidgetRoot, [mapType(j, `tempModule.${widgetMapName}`)], {tempModule: `./${className}`}, mainWidgetFileName);
}
// write file
fs.writeFileSync(mainWidgetFileName, crlfToLf(removeEmptyLinesBetweenImports(mainWidgetRoot.toSource(defaultRecastOptions))));
}
}
return root.toSource(defaultRecastOptions);
}
};
function getMainWidgetRoot(mainWidgetFileName) {
try {
let mainWidgetBuffer = fs.readFileSync(mainWidgetFileName);
return j(mainWidgetBuffer.toString());
} catch (error) {
// nop
}
return null;
}
function findObjectProperty(objectNode, propertyName) {
return objectNode.properties.find(
n =>
n.type === 'ObjectProperty' &&
n.key.type === 'Identifier' &&
n.key.name === propertyName
);
}
function findClassProperty(classMembers, propertyName) {
return classMembers.find(n =>
n.type === 'ClassProperty' &&
n.key.type === 'Identifier' &&
n.key.name === propertyName);
}
function findConstructor(classMembers) {
return classMembers.find(n =>
n.type === 'ClassMethod' &&
n.kind === 'constructor'
);
}
function findConstructorAssignmentExpression(constructorMembers, propertyName) {
return constructorMembers.find(n =>
n.type === 'ExpressionStatement' &&
n.expression.type === 'AssignmentExpression' &&
n.expression.left.type === 'MemberExpression' &&
n.expression.left.object.type === 'ThisExpression' &&
n.expression.left.property.type === 'Identifier' &&
n.expression.left.property.name === propertyName
);
}
function findConstructorSuperCall(constructorMembers) {
return constructorMembers.find(n =>
n.type === 'ExpressionStatement' &&
n.expression.type === 'CallExpression' &&
n.expression.callee.type === 'Super'
);
}
function findParentTablePath(columnPath) {
return findParentPathByObjectType(columnPath, isTable);
}
function findParentTableFieldPath(tablePath) {
return findParentPathByObjectType(tablePath, isTableField);
}
function findParentPathByObjectType(path, objectTypePredicate) {
return findParentPath(path, p => p.node.type === 'ObjectExpression' && objectTypePredicate((findObjectProperty(p.node, 'objectType') || {value: {}}).value.name));
}
function getId(node) {
let idProperty = findObjectProperty(node, 'id');
return ((idProperty || {}).value || {}).value;
}
function getObjectType(node) {
let objectTypeProperty = findObjectProperty(node, 'objectType');
return ((objectTypeProperty || {}).value || {}).name;
}
function getIdAndObjectType(node) {
let id = getId(node),
objectType = getObjectType(node);
return {id, objectType};
}
function createTableClassName(tableId, tableFieldId) {
if (tableId && tableId !== 'Table') {
return tableId.replaceAll('.', '');
}
if (tableFieldId) {
return tableFieldId.replaceAll('.', '') + 'Table';
}
throw new Error('At least one of tableId, tableFieldId must be set');
}
function getOrCreateExportedType(name, root, body) {
let candidates = root
.find(j.TSTypeAliasDeclaration)
.filter(/** NodePath<TSTypeAliasDeclaration, TSTypeAliasDeclaration> */path => path.node.id.name === name);
if (candidates.length) {
return candidates.get().node;
}
let type = j.tsTypeAliasDeclaration(j.identifier(name), j.tsTypeLiteral([]));
body.push(j.exportNamedDeclaration(type));
return type;
}
function getMembers(type) {
if (type.typeAnnotation.type === 'TSIntersectionType') {
return (type.typeAnnotation.types.find(t => t.type === 'TSTypeLiteral') || {members: []}).members;
}
return type.typeAnnotation.members;
}
function getOrCreateExportedClass(name, root, body) {
let candidates = root
.find(j.ClassDeclaration)
.filter(/** NodePath<ClassDeclaration, ClassDeclaration> */path => path.node.id.name === name);
if (candidates.length) {
return candidates.get().node;
}
let type = j.classDeclaration(j.identifier(name), j.classBody([]), null);
body.push(j.exportNamedDeclaration(type));
return type;
}
function createMapProperty(id, objectType) {
let identifier = j.identifier(`'${id}'`),
// add trailing ; to type, otherwise there is no ; at the end of the line when you use tsPropertySignature
typeAnnotation = createTypeAnnotation(`${addGenericIfNecessary(objectType)};`);
return j.tsPropertySignature(identifier, typeAnnotation);
}
function createClassProperty(id, objectType) {
let identifier = j.identifier(id),
typeAnnotation = createTypeAnnotation(addGenericIfNecessary(objectType));
return j.classProperty(identifier, null, typeAnnotation);
}
function createTypeAnnotation(objectType) {
return j.tsTypeAnnotation(j.tsTypeReference(j.identifier(objectType)));
}
function createAssignmentExpressionWithNull(propertyName) {
let identifier = j.identifier(propertyName),
member = j.memberExpression(j.thisExpression(), identifier),
assignment = j.assignmentExpression('=', member, j.nullLiteral());
return j.expressionStatement(assignment);
}
function createJsDocTypeComment(type) {
return j.commentBlock(`* @type ${type} `);
}
function isWidget(objectType) {
return objectType && !isColumn(objectType)
&& !objectType.endsWith('TableRow')
&& !objectType.endsWith('TreeNode')
&& !objectType.endsWith('Page')
&& objectType !== 'Status'
&& !objectType.endsWith('CodeType')
&& !objectType.endsWith('LookupCall');
}
function isColumn(objectType) {
return objectType && objectType.endsWith('Column');
}
function isTable(objectType) {
return objectType && objectType.endsWith('Table');
}
function isTableField(objectType) {
return objectType && objectType.endsWith('TableField');
}
function needsGeneric(objectType) {
return objectType && (objectType === 'SmartField'
|| objectType === 'SmartColumn'
|| objectType === 'ListBox'
|| objectType === 'TreeBox'
|| objectType === 'ModeSelectorField'
|| objectType === 'RadioButtonGroup'
|| objectType === 'RadioButton');
}
function addGenericIfNecessary(objectType) {
if (needsGeneric(objectType)) {
return `${objectType}<any>`;
}
return objectType;
}
export default widgetColumnMapPlugin;