[ACS-10126] Refactor migration logic to use native ts instead of ts-morph

This commit is contained in:
Aleksander Sklorz
2025-09-09 10:48:12 +02:00
parent e7024d1970
commit f936da4ea9

View File

@@ -15,8 +15,8 @@
* limitations under the License. * limitations under the License.
*/ */
import * as ts from 'typescript';
import { Rule, SchematicContext, SchematicsException, Tree } from '@angular-devkit/schematics'; import { Rule, SchematicContext, SchematicsException, Tree } from '@angular-devkit/schematics';
import { Project, NamedImports, SourceFile, ImportSpecifier, ImportDeclaration } from 'ts-morph';
interface MigrationData { interface MigrationData {
change: { change: {
@@ -68,17 +68,13 @@ const migrations: MigrationData[] = [alfrescoApiServiceMigration, alfrescoApiMoc
* @returns Schematic rule for updating imports * @returns Schematic rule for updating imports
*/ */
export function updateAlfrescoApiImports(): Rule { export function updateAlfrescoApiImports(): Rule {
const project = new Project();
// eslint-disable-next-line @typescript-eslint/no-unused-vars
return (tree: Tree, _context: SchematicContext) => { return (tree: Tree, _context: SchematicContext) => {
tree.visit((filePath: string) => visitor(filePath, tree, project)); tree.visit((filePath: string) => visitor(filePath, tree));
return tree; return tree;
}; };
} }
export const visitor = (filePath: string, tree: Pick<Tree, 'read' | 'overwrite'>, project: Project) => { export const visitor = (filePath: string, tree: Pick<Tree, 'read' | 'overwrite'>) => {
if ( if (
!filePath.includes('/.git/') && !filePath.includes('/.git/') &&
!filePath.includes('/node_modules/') && !filePath.includes('/node_modules/') &&
@@ -92,8 +88,11 @@ export const visitor = (filePath: string, tree: Pick<Tree, 'read' | 'overwrite'>
throw new SchematicsException(`Could not read file: ${filePath}`); throw new SchematicsException(`Could not read file: ${filePath}`);
} }
const fileContent = bufferFileContent.toString();
const sourceFile = ts.createSourceFile(filePath, fileContent, ts.ScriptTarget.Latest, true);
migrations.forEach((migrationData) => { migrations.forEach((migrationData) => {
const fileWithUpdatedImport = moveImport(filePath, bufferFileContent, project, migrationData); const fileWithUpdatedImport = moveImport(bufferFileContent, sourceFile, migrationData);
if (fileWithUpdatedImport) { if (fileWithUpdatedImport) {
tree.overwrite(filePath, fileWithUpdatedImport); tree.overwrite(filePath, fileWithUpdatedImport);
@@ -102,88 +101,70 @@ export const visitor = (filePath: string, tree: Pick<Tree, 'read' | 'overwrite'>
} }
}; };
const moveImport = (filePath: string, bufferFileContent: Buffer, project: Project, migrationData: MigrationData): string | undefined => { export const moveImport = (bufferFileContent: Buffer, sourceFile: ts.SourceFile, migrationData: MigrationData): string | undefined => {
const fileContent = bufferFileContent.toString(); const fileContent = bufferFileContent.toString();
const predictImport = fileContent.includes(migrationData.change.importedValue); const predictImport = fileContent.includes(migrationData.change.importedValue);
if (predictImport) { if (predictImport) {
const sourceFile = project.getSourceFile(`migration-${filePath}`) ?? project.createSourceFile(`migration-${filePath}`, fileContent); const moduleImports = sourceFile.statements.filter(ts.isImportDeclaration);
const alfrescoApiImportResult = getImportedValueFromSource(sourceFile, { const importDeclaration = moduleImports.find((moduleImport) => {
importedIdentifier: migrationData.change.importedValue, const currentImportSource = moduleImport.moduleSpecifier.getText().replace(/['"]/g, '');
from: migrationData.change.importSource return currentImportSource === migrationData.change.importSource;
}); });
if (alfrescoApiImportResult?.importedValue) { if (!importDeclaration || !importDeclaration.importClause?.namedBindings) {
if (alfrescoApiImportResult.allImportedValuesCount === 1) { return undefined;
// There is only one import e.g. import { A } from 'A';
// Therefore, we need to remove whole import statement
alfrescoApiImportResult.importSource?.remove();
} else {
alfrescoApiImportResult.importedValue?.remove();
} }
const alfrescoContentServiceImport = getSourceImport(sourceFile, migrationData.to.importSource); const namedBindings = importDeclaration.importClause.namedBindings;
if (!ts.isNamedImports(namedBindings)) {
return undefined;
}
if (alfrescoContentServiceImport) { const namedImportsElements = namedBindings.elements;
alfrescoContentServiceImport.addNamedImport(migrationData.to.importedValue); const importedValue = namedImportsElements.find((binding) => binding.name.text === migrationData.change.importedValue);
if (importedValue) {
let updatedContent = fileContent;
if (namedImportsElements.length === 1) {
updatedContent = removeTextRange(updatedContent, importDeclaration.getFullStart(), importDeclaration.getEnd());
} else { } else {
sourceFile.insertStatements( updatedContent = removeTextRange(updatedContent, importedValue.getFullStart(), importedValue.getEnd());
sourceFile.getImportDeclarations().length + 1, }
`import { ${migrationData.to.importedValue} } from '${migrationData.to.importSource}';`
const alreadyImported = moduleImports.some((moduleImport) => {
const currentImportSource = moduleImport.moduleSpecifier.getText().replace(/['"]/g, '');
return (
currentImportSource === migrationData.to.importSource &&
moduleImport.importClause?.namedBindings &&
ts.isNamedImports(moduleImport.importClause.namedBindings) &&
moduleImport.importClause.namedBindings.elements.some((el) => el.name.text === migrationData.to.importedValue)
); );
} });
return sourceFile.getFullText(); if (!alreadyImported) {
const firstNonImport = sourceFile.statements.find((stmt) => !ts.isImportDeclaration(stmt));
const insertPosition = firstNonImport ? firstNonImport.getFullStart() : fileContent.length;
updatedContent =
updatedContent.slice(0, insertPosition).replace(/\s*$/, '') + // usuń nadmiarowe linie
`\nimport { ${migrationData.to.importedValue} } from '${migrationData.to.importSource}';\n` +
updatedContent.slice(insertPosition);
}
updatedContent = updatedContent.replace(/^\s*\n+/g, '');
updatedContent = updatedContent.replace(/\n{3,}/g, '\n\n');
return updatedContent;
} }
} }
return undefined; return undefined;
}; };
const getSourceImport = (sourceFile: SourceFile, from: string): ImportDeclaration | undefined => { export const removeTextRange = (text: string, start: number, end: number): string => {
const moduleImports = sourceFile.getImportDeclarations(); return text.slice(0, start) + text.slice(end);
const importDeclaration = moduleImports.find((moduleImport) => {
const currentImportSource = moduleImport.getModuleSpecifierValue();
return currentImportSource === from;
});
return importDeclaration;
};
const getImportedValueFromSource = (
sourceFile: SourceFile,
searchedImport: {
importedIdentifier: string;
from: string;
}
): {
importedValue: ImportSpecifier | undefined;
importSource: ImportDeclaration | undefined;
allImportedValuesCount: number | undefined;
} => {
const importSource = getSourceImport(sourceFile, searchedImport.from);
if (!importSource) {
return {
importedValue: undefined,
importSource: undefined,
allImportedValuesCount: undefined
};
}
const importedValues = importSource?.getImportClause();
const namedImports = importedValues?.getNamedBindings() as NamedImports;
const namedImportsElements = namedImports?.getElements() ?? [];
const importedValue = namedImportsElements.find((binding) => binding.getName() === searchedImport.importedIdentifier);
return {
importedValue,
importSource,
allImportedValuesCount: namedImportsElements.length
};
}; };
export default updateAlfrescoApiImports; export default updateAlfrescoApiImports;