@progress/kendo-angular-dropdowns
Version:
A wide variety of native Angular dropdown components including AutoComplete, ComboBox, DropDownList, DropDownTree, MultiColumnComboBox, MultiSelect, and MultiSelectTree
969 lines • 57.9 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.tsInterfaceTransformer = exports.tsPropertyValueTransformer = exports.tsPropertyTransformer = exports.tsComponentPropertyRemoval = exports.attributeRemoval = exports.attributeValueUpdate = exports.attributeNameValueUpdate = exports.attributeNameUpdate = exports.eventUpdate = exports.htmlTransformer = exports.blockTextElements = void 0;
exports.hasKendoInTemplate = hasKendoInTemplate;
exports.isImportedFromPackage = isImportedFromPackage;
exports.tsPropertyRemoval = tsPropertyRemoval;
exports.blockTextElements = {
script: true,
noscript: true,
style: true,
pre: true,
};
function hasKendoInTemplate(source) {
const kendoPattern = /(<kendo-[^>\s]+|<[^>]*\s+kendo[A-Z][^>\s]*)/g;
return kendoPattern.test(source);
}
/**
* Checks if a specific type/interface is imported from the given package
* @param root - The JSCodeshift collection
* @param j - JSCodeshift instance
* @param packageName - The package name to check (e.g., '@progress/kendo-angular-dateinputs')
* @param typeName - The type/interface name to check (e.g., 'DatePickerComponent')
* @returns true if the type is imported from the package, false otherwise
*/
function isImportedFromPackage(root, j, packageName, typeName) {
let isImported = false;
root.find(j.ImportDeclaration).forEach((path) => {
if (path.node.source &&
path.node.source.value === packageName &&
path.node.specifiers) {
path.node.specifiers.forEach((specifier) => {
if (specifier.type === 'ImportSpecifier' &&
specifier.imported.type === 'Identifier' &&
specifier.imported.name === typeName) {
isImported = true;
}
});
}
});
return isImported;
}
/**
* Transforms HTML files and inline templates using a provided transformer function
*
* @param fileInfo - The file info containing path and source
* @param api - JSCodeshift API
* @param transformerFn - The transformer function to apply to template content
* @returns The transformed source code or undefined if no changes
*/
const htmlTransformer = (fileInfo, api, transformerFn) => {
const filePath = fileInfo.path;
if (filePath.endsWith('.html')) {
if (hasKendoInTemplate(fileInfo.source)) {
let updatedContent = fileInfo.source;
updatedContent = transformerFn(updatedContent);
return updatedContent;
}
return fileInfo.source;
}
const j = api.jscodeshift;
const rootSource = j(fileInfo.source);
// Transform inline templates using the provided transformer function
rootSource
.find(j.ClassDeclaration)
.forEach(classPath => {
const classNode = classPath.node;
if (!classNode.decorators || !classNode.decorators.length)
return;
const componentDecorator = classNode.decorators.find((decorator) => {
if (decorator.expression && decorator.expression.type === 'CallExpression') {
const callee = decorator.expression.callee;
if (callee.type === 'Identifier' && callee.name === 'Component') {
return true;
}
if (callee.type === 'MemberExpression' &&
callee.property &&
callee.property.type === 'Identifier' &&
callee.property.name === 'Component') {
return true;
}
}
return false;
});
if (!componentDecorator || !componentDecorator.expression)
return;
const expression = componentDecorator.expression;
if (expression.type !== 'CallExpression' || !expression.arguments.length)
return;
const componentOptions = expression.arguments[0];
if (componentOptions.type !== 'ObjectExpression')
return;
const props = componentOptions.properties || [];
const templateProp = props.find((prop) => (prop.key.type === 'Identifier' && prop.key.name === 'template') ||
(prop.key.type === 'StringLiteral' && prop.key.value === 'template'));
if (templateProp) {
let originalTemplate;
if (templateProp.value.type === 'StringLiteral' || templateProp.value.type === 'Literal') {
originalTemplate = templateProp.value.value;
}
else if (templateProp.value.type === 'TemplateLiteral') {
if (templateProp.value.quasis && templateProp.value.quasis.length) {
originalTemplate = templateProp.value.quasis
.map((q) => q.value.cooked || q.value.raw)
.join('');
}
else {
return;
}
}
else {
return;
}
if (hasKendoInTemplate(originalTemplate)) {
// Apply the provided transformer function
const transformedTemplate = transformerFn(originalTemplate);
if (transformedTemplate !== originalTemplate) {
if (templateProp.value.type === 'TemplateLiteral') {
templateProp.value = j.templateLiteral([j.templateElement({ cooked: transformedTemplate, raw: transformedTemplate }, true)], []);
}
else {
templateProp.value.value = transformedTemplate;
}
}
}
}
});
return rootSource.toSource();
};
exports.htmlTransformer = htmlTransformer;
const eventUpdate = (templateContent, tagName, oldEventName, newEventName) => {
// Escape special regex characters in tag name
const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
// Create regex pattern to match the tag with the old event binding
// Pattern matches: <tagName ...attributes... (oldEventName)="handler" ...>
const eventPattern = new RegExp(`(<${escapedTagName}[^>]*\\()${oldEventName}(\\)=)`, 'g');
// Replace old event name with new event name
const updatedContent = templateContent.replace(eventPattern, `$1${newEventName}$2`);
return updatedContent;
};
exports.eventUpdate = eventUpdate;
/**
* Transforms attributes in inline templates using regex patterns.
* This function handles bound ([attribute]), static (attribute="value"),
* two-way binding ([(attribute)]), and boolean (attribute) attributes
* within a specific tag using regular expressions.
*
* @param templateContent - The template string content to transform
* @param tagName - The HTML tag name to target (e.g., 'kendo-datepicker')
* @param attributeName - The current attribute name to replace
* @param newAttributeName - The new attribute name
* @returns The transformed template content
*/
const attributeNameUpdate = (templateContent, tagName, attributeName, newAttributeName) => {
// Escape special regex characters in tag and attribute names
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const escapedTag = escapeRegex(tagName);
const escapedAttr = escapeRegex(attributeName);
const escapedNewAttr = escapeRegex(newAttributeName);
// Pattern to match the opening tag with the attribute
// This pattern matches: <tagName ... attribute="value" ... > or <tagName ... [attribute]="value" ... >
// It captures the tag opening, everything before the attribute, the attribute binding type, and everything after
const boundAttributePattern = new RegExp(`(<${escapedTag}[^>]*?\\s+)\\[${escapedAttr}\\]\\s*=\\s*("(?:[^"\\\\]|\\\\.)*?"|'(?:[^'\\\\]|\\\\.)*?')([^>]*?>)`, 'gi');
const staticAttributePattern = new RegExp(`(<${escapedTag}[^>]*?\\s+)${escapedAttr}\\s*=\\s*("(?:[^"\\\\]|\\\\.)*?"|'(?:[^'\\\\]|\\\\.)*?')([^>]*?>)`, 'gi');
// Pattern for two-way data binding [(attribute)]="value"
const twoWayBindingPattern = new RegExp(`(<${escapedTag}[^>]*?\\s+)\\[\\(${escapedAttr}\\)\\]\\s*=\\s*("(?:[^"\\\\]|\\\\.)*?"|'(?:[^'\\\\]|\\\\.)*?')([^>]*?>)`, 'gi');
// Pattern for boolean attributes without values (e.g., <tag attribute>)
const booleanAttributePattern = new RegExp(`(<${escapedTag}[^>]*?\\s+)${escapedAttr}(\\s+[^>=]|\\s*[/>])`, 'gi');
// Replace two-way data binding [(attribute)]="value"
let result = templateContent.replace(twoWayBindingPattern, `$1[(${escapedNewAttr})]=$2$3`);
// Replace bound attributes [attribute]="value"
result = result.replace(boundAttributePattern, `$1[${escapedNewAttr}]=$2$3`);
// Replace static attributes attribute="value"
result = result.replace(staticAttributePattern, `$1${escapedNewAttr}=$2$3`);
// Replace boolean attributes attribute (without value)
result = result.replace(booleanAttributePattern, `$1${escapedNewAttr}$2`);
return result;
};
exports.attributeNameUpdate = attributeNameUpdate;
/**
* Transforms bound attributes with value property extraction using regex patterns.
* This function handles bound attributes and extracts a specific property from object literals
* or appends the property to variable references.
*
* @param templateContent - The template string content to transform
* @param tagName - The HTML tag name to target (e.g., 'kendo-chat')
* @param attributeName - The current attribute name to replace
* @param newAttributeName - The new attribute name
* @param valueProperty - The property to extract (e.g., 'id')
* @returns The transformed template content
*/
const attributeNameValueUpdate = (templateContent, tagName, attributeName, newAttributeName, valueProperty) => {
// Escape special regex characters in tag and attribute names
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const escapedTag = escapeRegex(tagName);
const escapedAttr = escapeRegex(attributeName);
const escapedNewAttr = escapeRegex(newAttributeName);
// Pattern to match bound attributes [attribute]="value"
// This captures: opening tag, bound attribute, and closing tag
const boundAttributePattern = new RegExp(`(<${escapedTag}[^>]*?\\s+)\\[${escapedAttr}\\]\\s*=\\s*("(?:[^"\\\\]|\\\\.)*?"|'(?:[^'\\\\]|\\\\.)*?'|[^\\s>]+)([^>]*?>)`, 'gi');
return templateContent.replace(boundAttributePattern, (match, beforeAttr, value, afterAttr) => {
// Remove quotes from value to analyze it
const trimmedValue = value.replace(/^["']|["']$/g, '');
// Check if it's an object literal (starts with { and ends with })
if (trimmedValue.trim().startsWith('{') && trimmedValue.trim().endsWith('}')) {
// For object literals like {id: 'foo'} or {id: 'foo', bar: 'baz'}
// We need to append .id inside the quotes if the value is quoted
if (value.match(/^["']/)) {
// Extract quote type and content, then add .id before the closing quote
const quoteType = value.charAt(0);
const content = value.slice(1, -1); // Remove quotes
const newValue = `${quoteType}${content}.${valueProperty}${quoteType}`;
return `${beforeAttr}[${escapedNewAttr}]=${newValue}${afterAttr}`;
}
else {
// Unquoted object literal
const newValue = `${value}.${valueProperty}`;
return `${beforeAttr}[${escapedNewAttr}]=${newValue}${afterAttr}`;
}
}
else {
// For variable references like "user"
// We append .id to the variable name
const newValue = value.replace(/^["'](.*)["']$/, (_match, content) => {
return `"${content}.${valueProperty}"`;
});
// If it's not quoted (bare variable), add quotes and property
if (!value.match(/^["']/)) {
return `${beforeAttr}[${escapedNewAttr}]="${trimmedValue}.${valueProperty}"${afterAttr}`;
}
return `${beforeAttr}[${escapedNewAttr}]=${newValue}${afterAttr}`;
}
});
};
exports.attributeNameValueUpdate = attributeNameValueUpdate;
/**
* Updates attribute values in HTML templates using regex patterns.
* This function handles both bound ([attribute]="'value'") and static (attribute="value") attributes
* and replaces old values with new values within a specific tag.
*
* @param templateContent - The template string content to transform
* @param tagName - The HTML tag name to target (e.g., 'kendo-toolbar')
* @param attributeName - The attribute name to target (e.g., 'showIcon')
* @param oldValue - The old attribute value to replace (e.g., 'overflow')
* @param newValue - The new attribute value (e.g., 'menu')
* @returns The transformed template content
*/
const attributeValueUpdate = (templateContent, tagName, attributeName, oldValue, newValue) => {
// Escape special regex characters in tag, attribute names, and values
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const escapedTag = escapeRegex(tagName);
const escapedAttr = escapeRegex(attributeName);
const escapedOldValue = escapeRegex(oldValue);
// Pattern for bound attributes [attribute]="'value'" (double quotes containing single quotes)
const boundDoubleSinglePattern = new RegExp(`(<${escapedTag}[^>]*?\\s+\\[${escapedAttr}\\]\\s*=\\s*")'${escapedOldValue}'("\\s*[^>]*?>)`, 'gi');
// Pattern for bound attributes [attribute]="\"value\"" (double quotes containing double quotes)
const boundDoubleDoublePattern = new RegExp(`(<${escapedTag}[^>]*?\\s+\\[${escapedAttr}\\]\\s*=\\s*")\\\\"${escapedOldValue}\\\\"("\\s*[^>]*?>)`, 'gi');
// Pattern for bound attributes [attribute]='value' (single quotes containing value)
const boundSinglePattern = new RegExp(`(<${escapedTag}[^>]*?\\s+\\[${escapedAttr}\\]\\s*=\\s*)'${escapedOldValue}'([^>]*?>)`, 'gi');
// Pattern for static attributes attribute="value"
const staticAttributePattern = new RegExp(`(<${escapedTag}[^>]*?\\s+${escapedAttr}\\s*=\\s*["'])${escapedOldValue}(["'][^>]*?>)`, 'gi');
// Replace bound attributes [attribute]="'overflow'" -> [attribute]="'menu'"
let result = templateContent.replace(boundDoubleSinglePattern, `$1'${newValue}'$2`);
// Replace bound attributes [attribute]="\"overflow\"" -> [attribute]="\"menu\""
result = result.replace(boundDoubleDoublePattern, `$1\\"${newValue}\\"$2`);
// Replace bound attributes [attribute]='overflow' -> [attribute]='menu'
result = result.replace(boundSinglePattern, `$1'${newValue}'$2`);
// Replace static attributes showIcon="overflow" -> showIcon="menu"
result = result.replace(staticAttributePattern, `$1${newValue}$2`);
return result;
};
exports.attributeValueUpdate = attributeValueUpdate;
/**
* Removes attributes from HTML templates using regex patterns.
* This function can remove entire attributes or specific properties from object literal attributes.
*
* @param templateContent - The template string content to transform
* @param tagName - The HTML tag name to target (e.g., 'kendo-chat')
* @param attributeName - The attribute name to remove or modify
* @param propertyToRemove - Optional: specific property to remove from object literal attributes
* @returns The transformed template content
*/
const attributeRemoval = (templateContent, tagName, attributeName, propertyToRemove) => {
// Escape special regex characters in tag and attribute names
const escapeRegex = (str) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const escapedTag = escapeRegex(tagName);
const escapedAttr = escapeRegex(attributeName);
// If no propertyToRemove is specified, remove the entire attribute
if (!propertyToRemove) {
// Remove bound attributes [attribute]="value"
const boundAttributePattern = new RegExp(`(\\s+)\\[${escapedAttr}\\]\\s*=\\s*("(?:[^"\\\\]|\\\\.)*?"|'(?:[^'\\\\]|\\\\.)*?'|[^\\s>]+)`, 'gi');
// Remove static attributes attribute="value"
const staticAttributePattern = new RegExp(`(\\s+)${escapedAttr}\\s*=\\s*("(?:[^"\\\\]|\\\\.)*?"|'(?:[^'\\\\]|\\\\.)*?'|[^\\s>]+)`, 'gi');
// Apply removals
let result = templateContent.replace(boundAttributePattern, '');
result = result.replace(staticAttributePattern, '');
return result;
}
// Remove specific property from object literal attributes
const boundAttributePattern = new RegExp(`(<${escapedTag}[^>]*?\\s+\\[${escapedAttr}\\]\\s*=\\s*)("(?:[^"\\\\]|\\\\.)*?"|'(?:[^'\\\\]|\\\\.)*?'|[^\\s>]+)([^>]*?>)`, 'gi');
return templateContent.replace(boundAttributePattern, (match, beforeAttr, value, afterAttr) => {
// Remove quotes from value to analyze it
const trimmedValue = value.replace(/^["']|["']$/g, '');
// Check if it's an object literal (starts with { and ends with })
if (trimmedValue.trim().startsWith('{') && trimmedValue.trim().endsWith('}')) {
const objectLiteral = trimmedValue.trim();
// Create regex to remove the specific property
const propRegex = new RegExp(`\\s*${escapeRegex(propertyToRemove)}\\s*:\\s*[^,}]+\\s*(,\\s*)?`, 'g');
let newObjectLiteral = objectLiteral.replace(propRegex, '');
// Clean up trailing comma before closing brace
newObjectLiteral = newObjectLiteral.replace(/,\s*}$/, '}');
// If the object is now empty, remove the entire attribute
if (newObjectLiteral === '{}') {
return beforeAttr.replace(/\s+\[[^\]]+\]\s*=\s*$/, '') + afterAttr;
}
else {
// Restore quotes if the original value was quoted
if (value.match(/^["']/)) {
const quoteType = value.charAt(0);
return `${beforeAttr}${quoteType}${newObjectLiteral}${quoteType}${afterAttr}`;
}
else {
return `${beforeAttr}${newObjectLiteral}${afterAttr}`;
}
}
}
else {
// For non-object literals, we can't remove a specific property
console.warn(`Cannot remove property '${propertyToRemove}' from non-object literal: ${value}`);
return match; // Return unchanged
}
});
};
exports.attributeRemoval = attributeRemoval;
function tsPropertyRemoval(source, rootSource, j, packageName, typeName, propertyName) {
if (source.includes(typeName)) {
if (!isImportedFromPackage(rootSource, j, packageName, typeName)) {
return;
}
const typeVariables = new Set();
// Find function parameters of target type
rootSource.find(j.Function).forEach((path) => {
if (path.node.params) {
path.node.params.forEach((param) => {
if (param.type === 'Identifier' &&
param.typeAnnotation &&
param.typeAnnotation.typeAnnotation?.type === 'TSTypeReference' &&
param.typeAnnotation.typeAnnotation.typeName?.type === 'Identifier' &&
param.typeAnnotation.typeAnnotation.typeName.name === typeName) {
typeVariables.add(param.name);
}
});
}
});
// Find local variables of target type
rootSource.find(j.VariableDeclarator).forEach((path) => {
if (path.node.id.type === 'Identifier' &&
path.node.id.typeAnnotation &&
path.node.id.typeAnnotation.typeAnnotation?.type === 'TSTypeReference' &&
path.node.id.typeAnnotation.typeAnnotation.typeName?.type === 'Identifier' &&
path.node.id.typeAnnotation.typeAnnotation.typeName.name === typeName) {
typeVariables.add(path.node.id.name);
}
});
// Find class properties of target type
rootSource.find(j.ClassProperty).forEach((path) => {
if (path.node.key.type === 'Identifier' &&
path.node.typeAnnotation &&
path.node.typeAnnotation.typeAnnotation &&
path.node.typeAnnotation.typeAnnotation.type === 'TSTypeReference' &&
path.node.typeAnnotation.typeAnnotation.typeName &&
path.node.typeAnnotation.typeAnnotation.typeName.type === 'Identifier' &&
path.node.typeAnnotation.typeAnnotation.typeName.name === typeName) {
typeVariables.add(path.node.key.name);
}
});
// Handle class properties with object expressions
rootSource
.find(j.ClassProperty)
.filter((path) => {
// Check if the property has the correct type annotation
return !!(path.node.typeAnnotation &&
path.node.typeAnnotation.typeAnnotation &&
path.node.typeAnnotation.typeAnnotation.type === 'TSTypeReference' &&
path.node.typeAnnotation.typeAnnotation.typeName &&
path.node.typeAnnotation.typeAnnotation.typeName.type === 'Identifier' &&
path.node.typeAnnotation.typeAnnotation.typeName.name === typeName);
})
.forEach((path) => {
if (path.node.value && path.node.value.type === 'ObjectExpression') {
const properties = path.node.value.properties;
const propIndex = properties.findIndex((p) => p.type === 'ObjectProperty' &&
p.key &&
p.key.type === 'Identifier' &&
p.key.name === propertyName);
if (propIndex !== -1) {
// Just remove the property, leaving an empty object if it was the only one
properties.splice(propIndex, 1);
}
}
});
// Handle variable declarations with object literal initializers
// e.g., const modelFields: ConversationalUIModelFields = { pinnedByField: 'value', id: 'bar' };
rootSource
.find(j.VariableDeclarator, {
id: {
type: 'Identifier',
typeAnnotation: {
typeAnnotation: {
type: 'TSTypeReference',
typeName: {
name: typeName,
},
},
},
},
})
.forEach((path) => {
if (path.node.init && path.node.init.type === 'ObjectExpression') {
const properties = path.node.init.properties;
const propIndex = properties.findIndex((p) => p.type === 'ObjectProperty' &&
p.key &&
p.key.type === 'Identifier' &&
p.key.name === propertyName);
if (propIndex !== -1) {
// Just remove the property, leaving an empty object if it was the only one
properties.splice(propIndex, 1);
}
}
});
// Handle return statements with object literals
rootSource.find(j.ReturnStatement).forEach((path) => {
if (path.node.argument && path.node.argument.type === 'ObjectExpression') {
const properties = path.node.argument.properties;
const propIndex = properties.findIndex((p) => p.type === 'ObjectProperty' &&
p.key &&
p.key.type === 'Identifier' &&
p.key.name === propertyName);
if (propIndex !== -1) {
properties.splice(propIndex, 1);
}
}
});
// Handle direct member expression assignments to properties of variables/parameters
// e.g., fields.pinnedByField = value; or this.chatModelFields.pinnedByField = value;
rootSource
.find(j.AssignmentExpression)
.filter((path) => {
const { left } = path.node;
if (left.type === 'MemberExpression' &&
left.property.type === 'Identifier' &&
left.property.name === propertyName) {
// Check if the object is a variable/parameter of our type
if (left.object.type === 'Identifier' && typeVariables.has(left.object.name)) {
return true;
}
// Check if it's this.property where property is of our type
if (left.object.type === 'MemberExpression' &&
left.object.object.type === 'ThisExpression' &&
left.object.property.type === 'Identifier' &&
typeVariables.has(left.object.property.name)) {
return true;
}
// Check for type assertions: (expr as Type).property
if (left.object.type === 'TSAsExpression' &&
left.object.typeAnnotation.type === 'TSTypeReference' &&
left.object.typeAnnotation.typeName?.type === 'Identifier' &&
left.object.typeAnnotation.typeName.name === typeName) {
return true;
}
}
return false;
})
.forEach((path) => {
// Remove the entire expression statement
const statement = j(path).closest(j.ExpressionStatement);
if (statement.length > 0) {
statement.remove();
}
});
// Handle nested member expressions like chatConfig.chat.modelFields.pinnedByField
rootSource
.find(j.AssignmentExpression, {
left: {
type: 'MemberExpression',
object: {
type: 'MemberExpression',
},
property: {
name: propertyName,
},
},
})
.forEach((path) => {
j(path).closest(j.ExpressionStatement).remove();
});
return rootSource;
}
}
const tsComponentPropertyRemoval = (source, root, j, packageName, componentType, componentProperty, propertyToRemove) => {
if (source.includes(componentType)) {
// Check if componentType is imported from the specified package
if (!isImportedFromPackage(root, j, packageName, componentType)) {
return;
}
// Find all class properties that are of type componentType
const properties = new Set();
root.find(j.ClassProperty, {
typeAnnotation: {
typeAnnotation: {
typeName: {
name: componentType,
},
},
},
}).forEach((path) => {
if (path.node.key.type === 'Identifier') {
properties.add(path.node.key.name);
}
});
// Find function parameters of type componentType
const parameters = new Set();
root.find(j.FunctionDeclaration).forEach((path) => {
if (path.node.params) {
path.node.params.forEach((param) => {
if (param.type === 'Identifier' &&
param.typeAnnotation &&
param.typeAnnotation.typeAnnotation?.type === 'TSTypeReference' &&
param.typeAnnotation.typeAnnotation.typeName.type === 'Identifier' &&
param.typeAnnotation.typeAnnotation.typeName.name === componentType) {
parameters.add(param.name);
}
});
}
});
// Also check method declarations in classes
root.find(j.ClassMethod).forEach((path) => {
if (path.node.params) {
path.node.params.forEach((param) => {
if (param.type === 'Identifier' &&
param.typeAnnotation &&
param.typeAnnotation.typeAnnotation?.type === 'TSTypeReference' &&
param.typeAnnotation.typeAnnotation.typeName.type === 'Identifier' &&
param.typeAnnotation.typeAnnotation.typeName.name === componentType) {
parameters.add(param.name);
}
});
}
});
// Also check arrow functions
root.find(j.ArrowFunctionExpression).forEach((path) => {
if (path.node.params) {
path.node.params.forEach((param) => {
if (param.type === 'Identifier' &&
param.typeAnnotation &&
param.typeAnnotation.typeAnnotation?.type === 'TSTypeReference' &&
param.typeAnnotation.typeAnnotation.typeName.type === 'Identifier' &&
param.typeAnnotation.typeAnnotation.typeName.name === componentType) {
parameters.add(param.name);
}
});
}
});
// Find local variable declarations of type componentType
const localVariables = new Set();
root.find(j.VariableDeclarator).forEach((path) => {
if (path.node.id.type === 'Identifier' &&
path.node.id.typeAnnotation &&
path.node.id.typeAnnotation.typeAnnotation?.type === 'TSTypeReference' &&
path.node.id.typeAnnotation.typeAnnotation.typeName.type === 'Identifier' &&
path.node.id.typeAnnotation.typeAnnotation.typeName.name === componentType) {
localVariables.add(path.node.id.name);
}
});
// Find array variables of type componentType[]
// This handles cases like: const arr: ChatComponent[] = [...]; arr[0].property = value;
const arrayVariables = new Set();
root.find(j.VariableDeclarator).forEach((path) => {
if (path.node.id.type === 'Identifier' &&
path.node.id.typeAnnotation &&
path.node.id.typeAnnotation.typeAnnotation?.type === 'TSArrayType' &&
path.node.id.typeAnnotation.typeAnnotation.elementType?.type === 'TSTypeReference' &&
path.node.id.typeAnnotation.typeAnnotation.elementType.typeName?.type === 'Identifier' &&
path.node.id.typeAnnotation.typeAnnotation.elementType.typeName.name === componentType) {
arrayVariables.add(path.node.id.name);
}
});
// Find object properties that have componentType (e.g., {chat: ChatComponent})
// This handles cases like: const config: {chat: ChatComponent} = {...}; config.chat.property = value;
const objectProperties = new Map(); // Maps variable name to property name
root.find(j.VariableDeclarator).forEach((path) => {
if (path.node.id.type === 'Identifier' &&
path.node.id.typeAnnotation &&
path.node.id.typeAnnotation.typeAnnotation?.type === 'TSTypeLiteral') {
const varName = path.node.id.name;
const members = path.node.id.typeAnnotation.typeAnnotation.members;
members.forEach((member) => {
if (member.type === 'TSPropertySignature' &&
member.key &&
member.key.type === 'Identifier' &&
member.typeAnnotation &&
member.typeAnnotation.typeAnnotation?.type === 'TSTypeReference' &&
member.typeAnnotation.typeAnnotation.typeName?.type === 'Identifier' &&
member.typeAnnotation.typeAnnotation.typeName.name === componentType) {
if (!objectProperties.has(varName)) {
objectProperties.set(varName, []);
}
objectProperties.get(varName).push(member.key.name);
}
});
}
});
// If no propertyToRemove is specified, remove the entire componentProperty
if (!propertyToRemove) {
// Handle direct property assignments like: foo.scrollable = value;
root.find(j.AssignmentExpression)
.filter((path) => {
const { left } = path.value;
// Check if this assigns to component.componentProperty
if (left &&
left.type === 'MemberExpression' &&
left.property &&
left.property.name === componentProperty) {
// Check if the base object is our component type
return isComponentTypeMatch(root, j, left.object, componentType);
}
return false;
})
.forEach((path) => {
// Remove the entire statement
j(path).closest(j.ExpressionStatement).remove();
});
return root;
}
// CASE 1: Handle direct property assignments like: foo.scrollable.mouseScrollSpeed = 3000;
root.find(j.AssignmentExpression)
.filter((path) => {
const { left } = path.value;
// Check if this is a member expression assignment
if (left && left.type === 'MemberExpression') {
// Check if we're accessing the property to remove
if (left.property && left.property.name === propertyToRemove) {
// Check if we're accessing it from component.componentProperty
const obj = left.object;
if (obj &&
obj.type === 'MemberExpression' &&
obj.property &&
obj.property.name === componentProperty) {
// Now check if the base object is our component type (includes all cases: this, parameters, variables, casts)
return isComponentTypeMatch(root, j, obj.object, componentType);
}
}
}
return false;
})
.forEach((path) => {
// Remove the entire statement
j(path).closest(j.ExpressionStatement).remove();
});
// CASE 2 & 3: Handle object assignments like: foo.scrollable = { mouseScrollSpeed: 3000, ... };
root.find(j.AssignmentExpression)
.filter((path) => {
const { left, right } = path.value;
// Check if this assigns to component.componentProperty
if (left &&
left.type === 'MemberExpression' &&
left.property &&
left.property.name === componentProperty &&
right &&
right.type === 'ObjectExpression') {
// Check if the base object is our component type (includes all cases: this, parameters, variables, casts)
return isComponentTypeMatch(root, j, left.object, componentType);
}
return false;
})
.forEach((path) => {
const properties = path.value.right.properties;
// Find the property we want to remove
const propIndex = properties.findIndex((p) => p &&
p.type === 'ObjectProperty' &&
p.key &&
p.key.type === 'Identifier' &&
p.key.name === propertyToRemove);
if (propIndex !== -1) {
// Case 2: If it's the only property, remove the entire statement
if (properties.length === 1) {
j(path).closest(j.ExpressionStatement).remove();
}
// Case 3: If there are other properties, just remove this one property
else {
properties.splice(propIndex, 1);
}
}
});
return root;
}
};
exports.tsComponentPropertyRemoval = tsComponentPropertyRemoval;
const tsPropertyTransformer = (source, root, j, packageName, componentType, propertyName, newPropertyName, valueProperty) => {
if (source.includes(componentType)) {
// Check if componentType is imported from the specified package
if (!isImportedFromPackage(root, j, packageName, componentType)) {
return;
}
// Find all class properties that are of type componentType
const properties = new Set();
// Find properties with type annotations
root.find(j.ClassProperty, {
typeAnnotation: {
typeAnnotation: {
typeName: {
name: componentType,
},
},
},
}).forEach((path) => {
if (path.node.key.type === 'Identifier') {
properties.add(path.node.key.name);
}
});
// Find function parameters of type componentType
const parameters = new Set();
root.find(j.FunctionDeclaration).forEach((path) => {
if (path.node.params) {
path.node.params.forEach((param) => {
if (param.type === 'Identifier' &&
param.typeAnnotation &&
param.typeAnnotation.typeAnnotation?.type === 'TSTypeReference' &&
param.typeAnnotation.typeAnnotation.typeName.type === 'Identifier' &&
param.typeAnnotation.typeAnnotation.typeName.name === componentType) {
parameters.add(param.name);
}
});
}
});
// Also check method declarations in classes
root.find(j.ClassMethod).forEach((path) => {
if (path.node.params) {
path.node.params.forEach((param) => {
if (param.type === 'Identifier' &&
param.typeAnnotation &&
param.typeAnnotation.typeAnnotation?.type === 'TSTypeReference' &&
param.typeAnnotation.typeAnnotation.typeName.type === 'Identifier' &&
param.typeAnnotation.typeAnnotation.typeName.name === componentType) {
parameters.add(param.name);
}
});
}
});
// Also check arrow functions
root.find(j.ArrowFunctionExpression).forEach((path) => {
if (path.node.params) {
path.node.params.forEach((param) => {
if (param.type === 'Identifier' &&
param.typeAnnotation &&
param.typeAnnotation.typeAnnotation?.type === 'TSTypeReference' &&
param.typeAnnotation.typeAnnotation.typeName.type === 'Identifier' &&
param.typeAnnotation.typeAnnotation.typeName.name === componentType) {
parameters.add(param.name);
}
});
}
});
// Find local variable declarations of type componentType
const localVariables = new Set();
root.find(j.VariableDeclarator).forEach((path) => {
if (path.node.id.type === 'Identifier' &&
path.node.id.typeAnnotation &&
path.node.id.typeAnnotation.typeAnnotation?.type === 'TSTypeReference' &&
path.node.id.typeAnnotation.typeAnnotation.typeName.type === 'Identifier' &&
path.node.id.typeAnnotation.typeAnnotation.typeName.name === componentType) {
localVariables.add(path.node.id.name);
}
});
// Find all member expressions where propertyName is accessed on any componentType instance
root.find(j.MemberExpression, {
property: {
type: 'Identifier',
name: propertyName,
},
})
.filter((path) => {
// Filter to only include accesses on properties that are componentType instances
if (path.node.object.type === 'MemberExpression' && path.node.object.property.type === 'Identifier') {
// handle properties of this
if (path.node.object.object.type === 'ThisExpression' &&
properties.has(path.node.object.property.name)) {
return true;
}
}
// Handle function parameters and local variables
if (path.node.object.type === 'Identifier') {
return parameters.has(path.node.object.name) || localVariables.has(path.node.object.name);
}
// Handle TypeScript type assertions like (this.componentProperty as ComponentType).property
if (path.node.object.type === 'TSAsExpression') {
const typeAssertion = path.node.object;
// Check if the type assertion is casting to our componentType
if (typeAssertion.typeAnnotation &&
typeAssertion.typeAnnotation.type === 'TSTypeReference' &&
typeAssertion.typeAnnotation.typeName &&
typeAssertion.typeAnnotation.typeName.type === 'Identifier' &&
typeAssertion.typeAnnotation.typeName.name === componentType) {
return true;
}
}
return false;
})
.forEach((path) => {
// Replace old property name with new property name
if (path.node.property.type === 'Identifier') {
path.node.property.name = newPropertyName;
}
// If valueProperty is specified and this is part of an assignment,
// we need to also modify the right-hand side of the assignment
if (valueProperty) {
const assignmentExpression = path.parent;
if (assignmentExpression &&
assignmentExpression.value &&
assignmentExpression.value.type === 'AssignmentExpression' &&
assignmentExpression.value.left === path.node) {
const rightSide = assignmentExpression.value.right;
// Case 1: Right side is a member expression (e.g., this.user, obj.user) -> transform to this.user.id, obj.user.id
// Case 2: Right side is an identifier (e.g., user, foo) -> transform to user.id, foo.id
if (rightSide.type === 'MemberExpression' || rightSide.type === 'Identifier') {
const newRightSide = j.memberExpression(rightSide, j.identifier(valueProperty));
assignmentExpression.value.right = newRightSide;
}
// Case 3: Right side is object literal -> extract the valueProperty value
else if (rightSide.type === 'ObjectExpression') {
// Find the property that matches valueProperty
const targetProperty = rightSide.properties.find((prop) => prop.type === 'ObjectProperty' &&
prop.key &&
prop.key.type === 'Identifier' &&
prop.key.name === valueProperty);
if (targetProperty) {
// Replace the entire object literal with just the value of the target property
assignmentExpression.value.right = targetProperty.value;
}
}
}
}
});
}
};
exports.tsPropertyTransformer = tsPropertyTransformer;
const tsPropertyValueTransformer = (source, root, j, packageName, typeName, oldValue, newValue) => {
if (source.includes(typeName)) {
// Check if typeName is imported from the specified package
if (!isImportedFromPackage(root, j, packageName, typeName)) {
return;
}
root.find(j.ClassProperty)
.filter((path) => {
if (path.node.typeAnnotation?.typeAnnotation &&
path.node.typeAnnotation.typeAnnotation.type === 'TSTypeReference' &&
path.node.typeAnnotation.typeAnnotation.typeName &&
path.node.typeAnnotation.typeAnnotation.typeName.type === 'Identifier' &&
path.node.typeAnnotation.typeAnnotation.typeName.name === typeName) {
return true;
}
return false;
})
.forEach((path) => {
if (path.node.value && path.node.value.type === 'StringLiteral' && path.node.value.value === oldValue) {
path.node.value.value = newValue;
}
});
root.find(j.VariableDeclarator)
.filter((path) => {
if (path.node.id.type === 'Identifier' &&
path.node.id.typeAnnotation?.typeAnnotation &&
path.node.id.typeAnnotation.typeAnnotation.type === 'TSTypeReference' &&
path.node.id.typeAnnotation.typeAnnotation.typeName.type === 'Identifier' &&
path.node.id.typeAnnotation.typeAnnotation.typeName.name === typeName) {
return true;
}
return false;
})
.forEach((path) => {
if (path.node.id.type === 'Identifier') {
if (path.node.init && path.node.init.type === 'StringLiteral' && path.node.init.value === oldValue) {
path.node.init.value = newValue;
}
}
});
root.find(j.AssignmentExpression)
.filter((path) => {
return path.node.right.type === 'StringLiteral' && path.node.right.value === oldValue;
})
.forEach((path) => {
path.node.right.value = newValue;
});
root.find(j.JSXAttribute, {
value: {
type: 'StringLiteral',
value: oldValue,
},
}).forEach((path) => {
if (path.node.value?.type === 'StringLiteral') {
path.node.value.value = newValue;
}
});
}
};
exports.tsPropertyValueTransformer = tsPropertyValueTransformer;
const tsInterfaceTransformer = (fileInfo, rootSource, j, packageName, interfaceName, newName) => {
const source = fileInfo.source;
if (source.includes(interfaceName)) {
// Check if interface is imported from the specified package and rename it
let isImported = false;
rootSource.find(j.ImportDeclaration).forEach((path) => {
if (path.node.source &&
path.node.source.value === packageName &&
path.node.specifiers) {
path.node.specifiers.forEach((specifier) => {
if (specifier.type === 'ImportSpecifier' &&
specifier.imported.type === 'Identifier' &&
specifier.imported.name === interfaceName) {
isImported = true;
specifier.imported.name = newName;
}
});
}
});
if (!isImported) {
return;
}
rootSource.find(j.ClassProperty).forEach((path) => {
if (path.node.typeAnnotation &&
path.node.typeAnnotation.typeAnnotation &&
path.node.typeAnnotation.typeAnnotation.type === 'TSTypeReference' &&
path.node.typeAnnotation.typeAnnotation.typeName &&
path.node.typeAnnotation.typeAnnotation.typeName.type === 'Identifier' &&
path.node.typeAnnotation.typeAnnotation.typeName.name === interfaceName) {
path.node.typeAnnotation.typeAnnotation.typeName.name = newName;
}
});
rootSource.find(j.VariableDeclarator).forEach((path) => {
if (path.node.id.type === 'Identifier' &&
path.node.id.typeAnnotation &&
path.node.id.typeAnnotation.typeAnnotation &&
path.node.id.typeAnnotation.typeAnnotation.type === 'TSTypeReference' &&
path.node.id.typeAnnotation.typeAnnotation.typeName &&
path.node.id.typeAnnotation.typeAnnotation.typeName.type === 'Identifier' &&
path.node.id.typeAnnotation.typeAnnotation.typeName.name === interfaceName) {
path.node.id.typeAnnotation.typeAnnotation.typeName.name = newName;
}
});
rootSource.find(j.FunctionDeclaration).forEach((path) => {
if (path.node.params) {
path.node.params.forEach((param) => {
if (param.type === 'Identifier' &&
param.typeAnnotation &&
param.typeAnnotation.typeAnnotation &&
param.typeAnnotation.typeAnnotation.type === 'TSTypeReference' &&
param.typeAnnotation.typeAnnotation.typeName &&
param.typeAnnotation.typeAnnotation.typeName.type === 'Identifier' &&
param.typeAnnotation.typeAnnotation.typeName.name === interfaceName) {
param.typeAnnotation.typeAnnotation.typeName.name = newName;
}
});
}
});
rootSource.find(j.ArrowFunctionExpression).forEach((path) => {
if (path.node.params