import { type Expression, type Node, type ObjectLiteralExpression, Project, type SourceFile, ts, } from "ts-morph" export function codemod(paths: string) { const project = new Project() project.addSourceFilesAtPaths(paths) project.getSourceFiles().forEach((file) => { processSourceFile(file) file.saveSync() }) } export function processSourceFile(file: SourceFile): void { file.getDescendantsOfKind(ts.SyntaxKind.CallExpression).forEach((call) => { const expression = call.getExpression().getText() if (expression === "intl.formatMessage") { const formatMessageArgs = call.getArguments() if (formatMessageArgs.length > 0) { const firstFormatMessageArg = formatMessageArgs[0] // Handle object literal in the first argument if ( firstFormatMessageArg.getKind() === ts.SyntaxKind.ObjectLiteralExpression ) { const expressionVariables = getVariableArguments( firstFormatMessageArg.asKindOrThrow( ts.SyntaxKind.ObjectLiteralExpression ) ) if (expressionVariables === "{}") { // No variables transformObjectLiteral( firstFormatMessageArg.asKindOrThrow( ts.SyntaxKind.ObjectLiteralExpression ) ) } else { // Handle variables const expressionReplacement = `intl.formatMessage(${transformObjectLiteralAndReturn( firstFormatMessageArg.asKindOrThrow( ts.SyntaxKind.ObjectLiteralExpression ) )}, ${expressionVariables})` call.replaceWithText(expressionReplacement) } } // Handle ternary expressions in the first argument else if ( firstFormatMessageArg.getKind() === ts.SyntaxKind.ConditionalExpression ) { const conditional = firstFormatMessageArg.asKindOrThrow( ts.SyntaxKind.ConditionalExpression ) const whenTrue = conditional.getWhenTrue() const whenFalse = conditional.getWhenFalse() // Check for variables in message const varArgsWhenTrue = getVariableArguments(whenTrue) const varArgsWhenFalse = getVariableArguments(whenFalse) // Replacements const whenTrueReplacement = varArgsWhenTrue !== "{}" ? `intl.formatMessage(${transformObjectLiteralAndReturn(whenTrue)}, ${varArgsWhenTrue})` : `intl.formatMessage(${transformObjectLiteralAndReturn(whenTrue)})` const whenFalseReplacement = varArgsWhenFalse !== "{}" ? `intl.formatMessage(${transformObjectLiteralAndReturn(whenFalse)}, ${varArgsWhenFalse})` : `intl.formatMessage(${transformObjectLiteralAndReturn(whenFalse)})` // Replace the ternary expression call.replaceWithText( `${conditional.getCondition().getText()} ? ${whenTrueReplacement} : ${whenFalseReplacement}` ) } } } }) // Format the file using its existing formatting rules file.formatText({ indentSize: 2, }) } // Helper function to transform object literals function transformObjectLiteral(objectLiteral: ObjectLiteralExpression): void { const idProperty = objectLiteral .getProperties() .find((prop) => { const p = prop.asKindOrThrow(ts.SyntaxKind.PropertyAssignment) return p.getName() === "id" }) ?.asKindOrThrow(ts.SyntaxKind.PropertyAssignment) if (idProperty) { const idValue = idProperty.getInitializer()?.getText() if (idValue) { // Add defaultMessage if ( !objectLiteral .getProperties() .some( (prop) => "getName" in prop && prop.getName() === "defaultMessage" ) ) { objectLiteral.addPropertyAssignment({ name: "defaultMessage", initializer: idValue, }) } // Remove the id property idProperty.remove() } } // Add description if not present // if ( // !objectLiteral // .getProperties() // .some((prop) => "getName" in prop && prop.getName() === "description") // ) { // objectLiteral.addPropertyAssignment({ // name: "description", // initializer: `{}`, // }) // } // Extract variables from the defaultMessage if present const defaultMessageProp = objectLiteral .getProperties() .find((prop) => "getName" in prop && prop.getName() === "defaultMessage") ?.asKindOrThrow(ts.SyntaxKind.PropertyAssignment) if (defaultMessageProp) { const defaultMessageValue = defaultMessageProp.getInitializer()?.getText() if (defaultMessageValue) { const extractedVariables = extractVariablesFromTemplateString(defaultMessageValue) if (extractedVariables.length > 0) { // Replace the variables in the defaultMessage with FormatJS placeholders const transformedMessage = replaceWithFormatJSPlaceholders( defaultMessageValue, extractedVariables ) defaultMessageProp.setInitializer(`${transformedMessage}`) } } } } // Helper function to extract variables from a template string function extractVariablesFromTemplateString(templateString: string): string[] { const regex = /\${(.*?)}/g const variables: string[] = [] let match // Find all variable references in the template literal while ((match = regex.exec(templateString)) !== null) { variables.push(match[1].trim()) } return variables } // Helper function to replace variables with FormatJS placeholders function replaceWithFormatJSPlaceholders( templateString: string, variables: string[] ): string { let transformedMessage = templateString variables.forEach((variable) => { transformedMessage = transformedMessage.replace( `\${${variable}}`, `{${variable}}` ) }) return transformedMessage } // Helper function to get the variables from the ternary branch function getVariableArguments(exp: Expression): string { const idProp = exp .asKindOrThrow(ts.SyntaxKind.ObjectLiteralExpression) .getProperties() .find((prop) => "getName" in prop && prop.getName() === "id") ?.asKindOrThrow(ts.SyntaxKind.PropertyAssignment) if (idProp) { const extractedVariables = extractVariablesFromTemplateString( idProp.getText() ) if (extractedVariables.length > 0) { return `{ ${extractedVariables.map((v) => `${v}: ${v}`).join(", ")} }` } } return "{}" // Return empty if no variables found } // Helper function to transform object literals and return text function transformObjectLiteralAndReturn(object: Node): string { if (object.getKind() === ts.SyntaxKind.ObjectLiteralExpression) { const obj = object.asKindOrThrow(ts.SyntaxKind.ObjectLiteralExpression) transformObjectLiteral(obj) return obj.getText() // Return the transformed object literal as text } return object.getText() }