diff --git a/src/migration/ember-app/index.ts b/src/migration/ember-app/index.ts index 3239e56..7dabc66 100644 --- a/src/migration/ember-app/index.ts +++ b/src/migration/ember-app/index.ts @@ -1,9 +1,9 @@ import type { CodemodOptions } from '../../types/index.js'; import { createOptions } from '../../utils/steps/create-options.js'; -import { updateClasses } from '../../utils/steps/update-classes.js'; +import { updateProject } from '../../utils/steps/update-project.js'; export function migrateEmberApp(codemodOptions: CodemodOptions): void { const options = createOptions(codemodOptions); - updateClasses(['app/**/*.{js,ts}'], options); + updateProject(['app/**/*.{js,ts}'], options); } diff --git a/src/migration/ember-v1-addon/index.ts b/src/migration/ember-v1-addon/index.ts index 262c9dd..36ce412 100644 --- a/src/migration/ember-v1-addon/index.ts +++ b/src/migration/ember-v1-addon/index.ts @@ -1,11 +1,11 @@ import type { CodemodOptions } from '../../types/index.js'; import { createOptions } from '../../utils/steps/create-options.js'; -import { updateClasses } from '../../utils/steps/update-classes.js'; +import { updateProject } from '../../utils/steps/update-project.js'; export function migrateEmberV1Addon(codemodOptions: CodemodOptions): void { const options = createOptions(codemodOptions); - updateClasses( + updateProject( ['addon/**/*.{js,ts}', 'tests/dummy/app/**/*.{js,ts}'], options, ); diff --git a/src/migration/ember-v2-addon/index.ts b/src/migration/ember-v2-addon/index.ts index 30a390f..b23be1d 100644 --- a/src/migration/ember-v2-addon/index.ts +++ b/src/migration/ember-v2-addon/index.ts @@ -1,9 +1,9 @@ import type { CodemodOptions } from '../../types/index.js'; import { createOptions } from '../../utils/steps/create-options.js'; -import { updateClasses } from '../../utils/steps/update-classes.js'; +import { updateProject } from '../../utils/steps/update-project.js'; export function migrateEmberV2Addon(codemodOptions: CodemodOptions): void { const options = createOptions(codemodOptions); - updateClasses(['src/**/*.{js,ts}'], options); + updateProject(['src/**/*.{js,ts}'], options); } diff --git a/src/utils/steps/update-classes/find-local-name.ts b/src/utils/steps/update-classes/find-local-name.ts deleted file mode 100644 index eedf95f..0000000 --- a/src/utils/steps/update-classes/find-local-name.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { AST } from '@codemod-utils/ast-javascript'; - -type Data = { - isTypeScript: boolean; -}; - -export function findLocalName(file: string, data: Data): string | undefined { - let localName: string | undefined; - - const traverse = AST.traverse(data.isTypeScript); - - traverse(file, { - // ... - }); - - return localName; -} diff --git a/src/utils/steps/update-classes/rename-imports.ts b/src/utils/steps/update-classes/rename-imports.ts deleted file mode 100644 index 15bc00a..0000000 --- a/src/utils/steps/update-classes/rename-imports.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { AST } from '@codemod-utils/ast-javascript'; - -type Data = { - isTypeScript: boolean; - localName: string; -}; - -export function renameImports(file: string, data: Data): string { - const traverse = AST.traverse(data.isTypeScript); - - const ast = traverse(file, { - // ... - }); - - return AST.print(ast); -} diff --git a/src/utils/steps/update-classes.ts b/src/utils/steps/update-project.ts similarity index 55% rename from src/utils/steps/update-classes.ts rename to src/utils/steps/update-project.ts index 4316dcd..fda7c99 100644 --- a/src/utils/steps/update-classes.ts +++ b/src/utils/steps/update-project.ts @@ -4,10 +4,9 @@ import { join } from 'node:path'; import { findFiles } from '@codemod-utils/files'; import { Options } from '../../types/index.js'; -import { findLocalName } from './update-classes/find-local-name.js'; -import { renameImports } from './update-classes/rename-imports.js'; +import { updateClass } from './update-project/update-class.js'; -export function updateClasses(src: string[], options: Options): void { +export function updateProject(src: string[], options: Options): void { const { projectRoot } = options; const filePaths = findFiles(src, { @@ -20,18 +19,7 @@ export function updateClasses(src: string[], options: Options): void { const isTypeScript = filePath.endsWith('.ts'); - const localName = findLocalName(oldFile, { - isTypeScript, - }); - - if (localName === undefined) { - return; - } - - const newFile = renameImports(oldFile, { - isTypeScript, - localName, - }); + const newFile = updateClass(oldFile, isTypeScript); writeFileSync(oldPath, newFile, 'utf8'); }); diff --git a/src/utils/steps/update-project/update-class.ts b/src/utils/steps/update-project/update-class.ts new file mode 100644 index 0000000..df46261 --- /dev/null +++ b/src/utils/steps/update-project/update-class.ts @@ -0,0 +1,157 @@ +import { AST } from '@codemod-utils/ast-javascript'; + +function isValueImport( + importKind: 'type' | 'typeof' | 'value' | undefined, +): boolean { + return importKind === undefined || importKind === 'value'; +} + +function updateImportStatement( + file: string, + data: { + isTypeScript: boolean; + }, +): { + localName: string | undefined; + newFile: string; +} { + let localName: string | undefined; + + const traverse = AST.traverse(data.isTypeScript); + + const ast = traverse(file, { + visitImportDeclaration(path) { + const { importKind, source, specifiers } = path.node; + + if (!isValueImport(importKind)) { + return false; + } + + if (source.type !== 'Literal' && source.type !== 'StringLiteral') { + return false; + } + + if (source.value !== '@ember/service' || !Array.isArray(specifiers)) { + return false; + } + + path.node.specifiers = specifiers.map((specifier) => { + // @ts-expect-error: 'specifier.importKind' exists + if (!isValueImport(specifier.importKind)) { + return specifier; + } + + if (specifier.type !== 'ImportSpecifier') { + return specifier; + } + + if ( + specifier.imported.name !== 'inject' && + specifier.imported.name !== 'service' + ) { + return specifier; + } + + localName = specifier.local!.name as string; + + return AST.builders.importSpecifier(AST.builders.identifier('service')); + }); + + return false; + }, + }); + + return { + localName, + newFile: AST.print(ast), + }; +} + +function updateServiceDecorators( + file: string, + data: { + isTypeScript: boolean; + localName: string; + }, +): string { + const traverse = AST.traverse(data.isTypeScript); + + const ast = traverse(file, { + visitCallExpression(node) { + this.traverse(node); + + switch (node.value.callee.type) { + case 'Identifier': { + if (node.value.callee.name === data.localName) { + node.value.callee.name = 'service'; + } + + break; + } + } + + return false; + }, + + visitClassProperty(node) { + if ( + !Array.isArray(node.value.decorators) || + node.value.decorators.length !== 1 + ) { + return false; + } + + const decorator = node.value.decorators[0]; + let isMatch = false; + + switch (decorator.expression.type) { + case 'CallExpression': { + if (decorator.expression.callee.name === data.localName) { + decorator.expression.callee.name = 'service'; + isMatch = true; + } + + break; + } + + case 'Identifier': { + if (decorator.expression.name === data.localName) { + decorator.expression.name = 'service'; + isMatch = true; + } + + break; + } + } + + if (isMatch && data.isTypeScript) { + node.value.accessibility = null; + node.value.declare = true; + node.value.definite = null; + node.value.readonly = null; + } + + return false; + }, + }); + + return AST.print(ast); +} + +export function updateClass(file: string, isTypeScript: boolean): string { + // eslint-disable-next-line prefer-const + let { localName, newFile } = updateImportStatement(file, { + isTypeScript, + }); + + if (!localName) { + return file; + } + + newFile = updateServiceDecorators(newFile, { + isTypeScript, + localName, + }); + + return newFile; +}