diff --git a/deno.json b/deno.json index 0d31bf2..03aeeac 100644 --- a/deno.json +++ b/deno.json @@ -2,6 +2,7 @@ "imports": { "@std/assert": "jsr:@std/assert@1", "@std/fs": "jsr:@std/fs@^1.0.19", + "@std/path": "jsr:@std/path@^1.0.0", "ts-morph": "jsr:@ts-morph/ts-morph@^26.0.0" }, "lint": { diff --git a/deno.lock b/deno.lock index dfc89b3..f0e55fe 100644 --- a/deno.lock +++ b/deno.lock @@ -83,6 +83,7 @@ "dependencies": [ "jsr:@std/assert@1", "jsr:@std/fs@^1.0.19", + "jsr:@std/path@1", "jsr:@ts-morph/ts-morph@26" ] } diff --git a/src/enum/enum.test.ts b/src/enum/enum.test.ts index ccc22c0..de558f7 100644 --- a/src/enum/enum.test.ts +++ b/src/enum/enum.test.ts @@ -1,7 +1,7 @@ import { executeTest } from "../utils/test.ts"; import { enumCodemod } from "./enum.ts"; -const testFn = await executeTest(enumCodemod); +const testFn = await executeTest(import.meta.dirname!, enumCodemod); Deno.test("converts string enum to object", async () => { await testFn({ diff --git a/src/parameter-properties/parameter-properties.test.ts b/src/parameter-properties/parameter-properties.test.ts index 27268d8..d1e6a0b 100644 --- a/src/parameter-properties/parameter-properties.test.ts +++ b/src/parameter-properties/parameter-properties.test.ts @@ -1,7 +1,10 @@ import { executeTest } from "../utils/test.ts"; import { parameterPropertiesCodemod } from "./parameter-properties.ts"; -const testFn = await executeTest(parameterPropertiesCodemod); +const testFn = await executeTest( + import.meta.dirname!, + parameterPropertiesCodemod, +); Deno.test("converts public parameter properties", async () => { await testFn({ diff --git a/src/path-alias/path-alias.test.ts b/src/path-alias/path-alias.test.ts new file mode 100644 index 0000000..f7b3060 --- /dev/null +++ b/src/path-alias/path-alias.test.ts @@ -0,0 +1,100 @@ +import { executeTest } from "../utils/test.ts"; +import { pathAliasCodemod } from "./path-alias.ts"; + +const testFn = await executeTest(import.meta.dirname!, pathAliasCodemod); + +Deno.test.only("converts simple path alias to relative path", async () => { + await testFn({ + testFile: "simple_alias_test.ts", + inFile: "src/path-alias/spec/simple-alias.in.ts", + outFile: "src/path-alias/spec/simple-alias.out.ts", + }); +}); + +Deno.test("converts wildcard path alias to relative path", async () => { + await testFn({ + testFile: "wildcard_alias_test.ts", + inFile: "src/path-alias/spec/wildcard-alias.in.ts", + outFile: "src/path-alias/spec/wildcard-alias.out.ts", + }); +}); + +Deno.test("handles multiple imports with different aliases", async () => { + await testFn({ + testFile: "multiple_imports_test.ts", + inFile: "src/path-alias/spec/multiple-imports.in.ts", + outFile: "src/path-alias/spec/multiple-imports.out.ts", + }); +}); + +Deno.test("preserves relative imports unchanged", async () => { + await testFn({ + testFile: "relative_imports_test.ts", + inFile: "src/path-alias/spec/relative-imports.in.ts", + outFile: "src/path-alias/spec/relative-imports.out.ts", + }); +}); + +Deno.test("preserves external package imports unchanged", async () => { + await testFn({ + testFile: "external_packages_test.ts", + inFile: "src/path-alias/spec/external-packages.in.ts", + outFile: "src/path-alias/spec/external-packages.out.ts", + }); +}); + +Deno.test("handles complex nested path aliases", async () => { + await testFn({ + testFile: "nested_aliases_test.ts", + inFile: "src/path-alias/spec/nested-aliases.in.ts", + outFile: "src/path-alias/spec/nested-aliases.out.ts", + }); +}); + +Deno.test("handles mixed import types", async () => { + await testFn({ + testFile: "mixed_imports_test.ts", + inFile: "src/path-alias/spec/mixed-imports.in.ts", + outFile: "src/path-alias/spec/mixed-imports.out.ts", + }); +}); + +Deno.test("handles type-only imports", async () => { + await testFn({ + testFile: "type_imports_test.ts", + inFile: "src/path-alias/spec/type-imports.in.ts", + outFile: "src/path-alias/spec/type-imports.out.ts", + }); +}); + +Deno.test("handles default and named imports", async () => { + await testFn({ + testFile: "default_named_imports_test.ts", + inFile: "src/path-alias/spec/default-named-imports.in.ts", + outFile: "src/path-alias/spec/default-named-imports.out.ts", + }); +}); + +Deno.test("handles re-exports", async () => { + await testFn({ + testFile: "re_exports_test.ts", + inFile: "src/path-alias/spec/re-exports.in.ts", + outFile: "src/path-alias/spec/re-exports.out.ts", + }); +}); + +Deno.test("handles dynamic imports", async () => { + await testFn({ + testFile: "dynamic_imports_test.ts", + inFile: "src/path-alias/spec/dynamic-imports.in.ts", + outFile: "src/path-alias/spec/dynamic-imports.out.ts", + }); +}); + +Deno.test("comprehensive path alias transformation", async () => { + await testFn({ + testFile: "comprehensive_test.ts", + inFile: "src/path-alias/spec/comprehensive.in.ts", + outFile: "src/path-alias/spec/comprehensive.out.ts", + }); +}); diff --git a/src/path-alias/path-alias.ts b/src/path-alias/path-alias.ts new file mode 100644 index 0000000..847e53c --- /dev/null +++ b/src/path-alias/path-alias.ts @@ -0,0 +1,233 @@ +import { Project, SourceFile, StringLiteral, SyntaxKind } from "ts-morph"; +import { dirname, join, relative } from "@std/path"; +import { existsSync } from "@std/fs/exists"; + +const project = new Project(); + +interface PathMapping { + [key: string]: string; +} + +function parseTsConfigPaths(tsConfigPath: string): PathMapping { + try { + const tsConfigContent = Deno.readTextFileSync(tsConfigPath); + const tsConfig = JSON.parse(tsConfigContent); + return tsConfig.compilerOptions?.paths || {}; + } catch { + return {}; + } +} + +function findTsConfigPath(filePath: string): string | null { + const dir = dirname(filePath); + const possiblePaths = [ + join(dir, "tsconfig.json"), + join(dir, "tsconfig.base.json"), + join(dirname(dir), "tsconfig.json"), + join(dirname(dir), "tsconfig.base.json"), + join(dirname(dirname(dir)), "tsconfig.json"), + join(dirname(dirname(dir)), "tsconfig.base.json"), + ]; + + for (const path of possiblePaths) { + try { + Deno.statSync(path); + return path; + } catch { + // File doesn't exist, continue + } + } + + return null; +} + +function resolvePathAlias( + importPath: string, + pathMappings: PathMapping, + currentFilePath: string, +): string | null { + // Find the matching path mapping + for (const [alias, targets] of Object.entries(pathMappings)) { + // Handle array of targets (TypeScript allows multiple targets) + const targetArray = Array.isArray(targets) ? targets : [targets]; + + for (const target of targetArray) { + // Create regex pattern for the alias + const aliasPattern = alias.replace("*", "([^/]+)"); + const regex = new RegExp(`^${aliasPattern}$`); + + if (regex.test(importPath)) { + // Extract the wildcard part + const match = importPath.match(regex); + if (!match) continue; + + const wildcardPart = match[1]; + + // Replace the wildcard in the target + const resolvedPath = target.replace("*", wildcardPart); + + // Convert to relative path from current file location + const currentDir = dirname(currentFilePath); + + // Find the tsconfig.json file to determine the base directory + const tsConfigPath = findTsConfigPath(currentFilePath); + const baseDir = tsConfigPath + ? dirname(tsConfigPath) + : dirname(currentFilePath); + + // Calculate relative path from current file to the resolved path + const relativePath = relative(currentDir, join(baseDir, resolvedPath)); + + // Add .ts extension if not present + if ( + !relativePath.endsWith(".ts") && !relativePath.endsWith(".tsx") && + !relativePath.endsWith(".js") && !relativePath.endsWith(".jsx") + ) { + return relativePath.startsWith(".") + ? `${relativePath}.ts` + : `./${relativePath}.ts`; + } + + return relativePath.startsWith(".") + ? relativePath + : `./${relativePath}`; + } + } + } + + return null; +} + +function _getPackageJsonDependencyNames( + filePath: string, +): string[] { + const packageJsonPath = join(dirname(filePath), "package.json"); + if (!existsSync(packageJsonPath)) { + throw new Error( + "package.json not found. This is required to detect external package dependencies.", + ); + } + + const packageJson = JSON.parse(Deno.readTextFileSync(packageJsonPath)); + const allDeps = { + ...packageJson.dependencies, + ...packageJson.devDependencies, + }; + return Object.keys(allDeps); +} + +function convertPathAliases(sourceFile: SourceFile): void { + const tsConfigPath = findTsConfigPath(sourceFile.getFilePath()); + if (!tsConfigPath) return; + + const pathMappings = parseTsConfigPaths(tsConfigPath); + if (Object.keys(pathMappings).length === 0) return; + + // Handle import declarations + const importDeclarations = sourceFile.getImportDeclarations(); + + importDeclarations.forEach((importDecl) => { + const moduleSpecifier = importDecl.getModuleSpecifier(); + + if (moduleSpecifier.getKind() === SyntaxKind.StringLiteral) { + const importPath = (moduleSpecifier as StringLiteral).getLiteralValue(); + + if ( + importPath.startsWith(".") || importPath.startsWith("/") || + !importPath.includes("/") || importPath.includes("node_modules") + // Common external packages that shouldn't be transformed + // TODO use package.json to detect external packages + ) { + return; + } + + const relativePath = resolvePathAlias( + importPath, + pathMappings, + sourceFile.getFilePath(), + ); + + if (relativePath) { + moduleSpecifier.replaceWithText(`"${relativePath}"`); + } + } + }); + + const exportDeclarations = sourceFile.getExportDeclarations(); + + exportDeclarations.forEach((exportDecl) => { + const moduleSpecifier = exportDecl.getModuleSpecifier(); + + if ( + moduleSpecifier && moduleSpecifier.getKind() === SyntaxKind.StringLiteral + ) { + const exportPath = (moduleSpecifier as StringLiteral).getLiteralValue(); + + // Skip if it's already a relative path or external package + if ( + exportPath.startsWith(".") || exportPath.startsWith("/") || + !exportPath.includes("/") || exportPath.includes("node_modules") + // Common external packages that shouldn't be transformed + // TODO use package.json to detect external packages + ) { + return; + } + + const relativePath = resolvePathAlias( + exportPath, + pathMappings, + sourceFile.getFilePath(), + ); + + if (relativePath) { + moduleSpecifier.replaceWithText(`"${relativePath}"`); + } + } + }); + + const callExpressions = sourceFile.getDescendantsOfKind( + SyntaxKind.CallExpression, + ); + + callExpressions.forEach((callExpr) => { + const expression = callExpr.getExpression(); + if (expression.getText() === "import") { + const arguments_ = callExpr.getArguments(); + if (arguments_.length > 0) { + const firstArg = arguments_[0]; + if (firstArg.getKind() === SyntaxKind.StringLiteral) { + const importPath = (firstArg as StringLiteral).getLiteralValue(); + + // Skip if it's already a relative path or external package + if ( + importPath.startsWith(".") || importPath.startsWith("/") || + !importPath.includes("/") || importPath.includes("node_modules") + // Common external packages that shouldn't be transformed + // TODO use package.json to detect external packages + ) { + return; + } + + const relativePath = resolvePathAlias( + importPath, + pathMappings, + sourceFile.getFilePath(), + ); + + if (relativePath) { + firstArg.replaceWithText(`"${relativePath}"`); + } + } + } + } + }); +} + +export function pathAliasCodemod(filePath: string): void { + // const packageNames = getPackageJsonDependencyNames("todo"); + const sourceFile = project.addSourceFileAtPath(filePath); + + convertPathAliases(sourceFile); + + sourceFile.saveSync(); +} diff --git a/src/path-alias/spec/comprehensive.in.ts b/src/path-alias/spec/comprehensive.in.ts new file mode 100644 index 0000000..3f15db3 --- /dev/null +++ b/src/path-alias/spec/comprehensive.in.ts @@ -0,0 +1,53 @@ +// Type-only imports +import type { UserType } from "@types/user"; +import type { ConfigType } from "@config/types"; + +// Regular imports with path aliases +import { UserService } from "@services/user"; +import { AuthService } from "@services/auth"; +import { Logger } from "@utils/logger"; +import { Database } from "@database/connection"; + +// External packages (should be preserved) +import express from "express"; +import { z } from "zod"; + +// Relative imports (should be preserved) +import { Helper } from "./helper"; +import { Constants } from "../constants"; + +// Wildcard imports +import { UserModel } from "@models/*"; +import { ValidationError } from "@types/errors"; + +// Re-exports +export { UserService } from "@services/user"; +export { AuthService } from "@services/auth"; + +// Default imports +import UserController from "@controllers/user"; + +export class App { + constructor( + private userService: UserService, + private authService: AuthService, + private logger: Logger, + private db: Database, + private helper: Helper, + ) {} + + async initialize() { + this.logger.info("Initializing application"); + await this.db.connect(); + } + + async createUser(userData: UserType) { + const validationSchema = z.object({ + name: z.string(), + email: z.string().email(), + }); + + const validatedData = validationSchema.parse(userData); + return this.userService.create(validatedData); + } +} diff --git a/src/path-alias/spec/comprehensive.out.ts b/src/path-alias/spec/comprehensive.out.ts new file mode 100644 index 0000000..cce6db0 --- /dev/null +++ b/src/path-alias/spec/comprehensive.out.ts @@ -0,0 +1,53 @@ +// Type-only imports +import type { UserType } from "../src/types/user.ts"; +import type { ConfigType } from "../src/config/types.ts"; + +// Regular imports with path aliases +import { UserService } from "../src/services/user.ts"; +import { AuthService } from "../src/services/auth.ts"; +import { Logger } from "../src/utils/logger.ts"; +import { Database } from "../src/database/connection.ts"; + +// External packages (should be preserved) +import express from "express"; +import { z } from "zod"; + +// Relative imports (should be preserved) +import { Helper } from "./helper"; +import { Constants } from "../constants"; + +// Wildcard imports +import { UserModel } from "../src/models/index.ts"; +import { ValidationError } from "../src/types/errors.ts"; + +// Re-exports +export { UserService } from "../src/services/user.ts"; +export { AuthService } from "../src/services/auth.ts"; + +// Default imports +import UserController from "../src/controllers/user.ts"; + +export class App { + constructor( + private userService: UserService, + private authService: AuthService, + private logger: Logger, + private db: Database, + private helper: Helper, + ) {} + + async initialize() { + this.logger.info("Initializing application"); + await this.db.connect(); + } + + async createUser(userData: UserType) { + const validationSchema = z.object({ + name: z.string(), + email: z.string().email(), + }); + + const validatedData = validationSchema.parse(userData); + return this.userService.create(validatedData); + } +} diff --git a/src/path-alias/spec/constants.ts b/src/path-alias/spec/constants.ts new file mode 100644 index 0000000..51e7641 --- /dev/null +++ b/src/path-alias/spec/constants.ts @@ -0,0 +1,4 @@ +export class Constants { + static API_URL = "http://localhost:3000"; + static PORT = 3000; +} diff --git a/src/path-alias/spec/default-named-imports.in.ts b/src/path-alias/spec/default-named-imports.in.ts new file mode 100644 index 0000000..ffcb778 --- /dev/null +++ b/src/path-alias/spec/default-named-imports.in.ts @@ -0,0 +1,18 @@ +import UserController from "@controllers/user"; +import { UserService } from "@services/user"; +import { AuthConfig, AuthService } from "@services/auth"; +import Logger, { LogLevel } from "@utils/logger"; + +export class App { + constructor( + private userController: UserController, + private userService: UserService, + private authService: AuthService, + private logger: Logger, + ) {} + + async start() { + this.logger.log("Starting app", LogLevel.INFO); + await this.authService.initialize(); + } +} diff --git a/src/path-alias/spec/default-named-imports.out.ts b/src/path-alias/spec/default-named-imports.out.ts new file mode 100644 index 0000000..ef535e0 --- /dev/null +++ b/src/path-alias/spec/default-named-imports.out.ts @@ -0,0 +1,18 @@ +import UserController from "../src/controllers/user.ts"; +import { UserService } from "../src/services/user.ts"; +import { AuthConfig, AuthService } from "../src/services/auth.ts"; +import Logger, { LogLevel } from "../src/utils/logger.ts"; + +export class App { + constructor( + private userController: UserController, + private userService: UserService, + private authService: AuthService, + private logger: Logger, + ) {} + + async start() { + this.logger.log("Starting app", LogLevel.INFO); + await this.authService.initialize(); + } +} diff --git a/src/path-alias/spec/dynamic-imports.in.ts b/src/path-alias/spec/dynamic-imports.in.ts new file mode 100644 index 0000000..aaeba5f --- /dev/null +++ b/src/path-alias/spec/dynamic-imports.in.ts @@ -0,0 +1,20 @@ +import { UserService } from "@services/user"; + +export class UserController { + constructor(private userService: UserService) {} + + async loadUserModule() { + const UserModule = await import("@modules/user"); + return new UserModule.default(); + } + + async loadAuthModule() { + const { AuthService } = await import("@services/auth"); + return new AuthService(); + } + + async loadConfig() { + const config = await import("@config/app"); + return config.default; + } +} diff --git a/src/path-alias/spec/dynamic-imports.out.ts b/src/path-alias/spec/dynamic-imports.out.ts new file mode 100644 index 0000000..257e295 --- /dev/null +++ b/src/path-alias/spec/dynamic-imports.out.ts @@ -0,0 +1,20 @@ +import { UserService } from "../src/services/user.ts"; + +export class UserController { + constructor(private userService: UserService) {} + + async loadUserModule() { + const UserModule = await import("../src/modules/user.ts"); + return new UserModule.default(); + } + + async loadAuthModule() { + const { AuthService } = await import("../src/services/auth.ts"); + return new AuthService(); + } + + async loadConfig() { + const config = await import("../src/config/app.ts"); + return config.default; + } +} diff --git a/src/path-alias/spec/external-packages.in.ts b/src/path-alias/spec/external-packages.in.ts new file mode 100644 index 0000000..3b8a475 --- /dev/null +++ b/src/path-alias/spec/external-packages.in.ts @@ -0,0 +1,22 @@ +import { UserService } from "@services/user"; +import express from "express"; +import { Request, Response } from "express"; +import { z } from "zod"; +import { Logger } from "@utils/logger"; + +export class UserController { + constructor( + private userService: UserService, + private logger: Logger, + ) {} + + async getUser(req: Request, res: Response) { + const schema = z.object({ + id: z.string(), + }); + + const { id } = schema.parse(req.params); + this.logger.info(`Fetching user ${id}`); + return this.userService.findById(id); + } +} diff --git a/src/path-alias/spec/external-packages.out.ts b/src/path-alias/spec/external-packages.out.ts new file mode 100644 index 0000000..612bb0f --- /dev/null +++ b/src/path-alias/spec/external-packages.out.ts @@ -0,0 +1,22 @@ +import { UserService } from "../src/services/user.ts"; +import express from "express"; +import { Request, Response } from "express"; +import { z } from "zod"; +import { Logger } from "../src/utils/logger.ts"; + +export class UserController { + constructor( + private userService: UserService, + private logger: Logger, + ) {} + + async getUser(req: Request, res: Response) { + const schema = z.object({ + id: z.string(), + }); + + const { id } = schema.parse(req.params); + this.logger.info(`Fetching user ${id}`); + return this.userService.findById(id); + } +} diff --git a/src/path-alias/spec/helper.ts b/src/path-alias/spec/helper.ts new file mode 100644 index 0000000..8aed9c9 --- /dev/null +++ b/src/path-alias/spec/helper.ts @@ -0,0 +1,5 @@ +export class Helper { + static formatName(name: string) { + return name.toUpperCase(); + } +} diff --git a/src/path-alias/spec/mixed-imports.in.ts b/src/path-alias/spec/mixed-imports.in.ts new file mode 100644 index 0000000..2ace424 --- /dev/null +++ b/src/path-alias/spec/mixed-imports.in.ts @@ -0,0 +1,28 @@ +// Path aliases +import { UserService } from "@services/user"; +import { Logger } from "@utils/logger"; + +// External packages +import express from "express"; +import { z } from "zod"; + +// Relative imports +import { Helper } from "./helper"; +import { Constants } from "../constants"; + +// Type imports +import type { UserType } from "@types/user"; + +export class App { + constructor( + private userService: UserService, + private logger: Logger, + private helper: Helper, + ) {} + + async start() { + this.logger.info("Starting app"); + const app = express(); + return app; + } +} diff --git a/src/path-alias/spec/mixed-imports.out.ts b/src/path-alias/spec/mixed-imports.out.ts new file mode 100644 index 0000000..8d7fd70 --- /dev/null +++ b/src/path-alias/spec/mixed-imports.out.ts @@ -0,0 +1,28 @@ +// Path aliases +import { UserService } from "../src/services/user.ts"; +import { Logger } from "../src/utils/logger.ts"; + +// External packages +import express from "express"; +import { z } from "zod"; + +// Relative imports +import { Helper } from "./helper"; +import { Constants } from "../constants"; + +// Type imports +import type { UserType } from "../src/types/user.ts"; + +export class App { + constructor( + private userService: UserService, + private logger: Logger, + private helper: Helper, + ) {} + + async start() { + this.logger.info("Starting app"); + const app = express(); + return app; + } +} diff --git a/src/path-alias/spec/multiple-imports.in.ts b/src/path-alias/spec/multiple-imports.in.ts new file mode 100644 index 0000000..37f5688 --- /dev/null +++ b/src/path-alias/spec/multiple-imports.in.ts @@ -0,0 +1,20 @@ +import { UserService } from "@services/user"; +import { AuthService } from "@services/auth"; +import { Logger } from "@utils/logger"; +import { Config } from "@config/app"; +import { Database } from "@database/connection"; + +export class App { + constructor( + private userService: UserService, + private authService: AuthService, + private logger: Logger, + private config: Config, + private db: Database, + ) {} + + async start() { + this.logger.info("Starting application"); + await this.db.connect(); + } +} diff --git a/src/path-alias/spec/multiple-imports.out.ts b/src/path-alias/spec/multiple-imports.out.ts new file mode 100644 index 0000000..cb78038 --- /dev/null +++ b/src/path-alias/spec/multiple-imports.out.ts @@ -0,0 +1,20 @@ +import { UserService } from "../src/services/user.ts"; +import { AuthService } from "../src/services/auth.ts"; +import { Logger } from "../src/utils/logger.ts"; +import { Config } from "../src/config/app.ts"; +import { Database } from "../src/database/connection.ts"; + +export class App { + constructor( + private userService: UserService, + private authService: AuthService, + private logger: Logger, + private config: Config, + private db: Database, + ) {} + + async start() { + this.logger.info("Starting application"); + await this.db.connect(); + } +} diff --git a/src/path-alias/spec/nested-aliases.in.ts b/src/path-alias/spec/nested-aliases.in.ts new file mode 100644 index 0000000..4fa58b2 --- /dev/null +++ b/src/path-alias/spec/nested-aliases.in.ts @@ -0,0 +1,23 @@ +import { UserService } from "@services/user"; +import { AuthService } from "@services/auth"; +import { Logger } from "@utils/logger"; +import { Database } from "@database/connection"; +import { Config } from "@config/app"; +import { ValidationError } from "@types/errors"; +import { UserModel } from "@models/user"; +import { AuthModel } from "@models/auth"; + +export class App { + constructor( + private userService: UserService, + private authService: AuthService, + private logger: Logger, + private db: Database, + private config: Config, + ) {} + + async initialize() { + this.logger.info("Initializing application"); + await this.db.connect(); + } +} diff --git a/src/path-alias/spec/nested-aliases.out.ts b/src/path-alias/spec/nested-aliases.out.ts new file mode 100644 index 0000000..0781b1a --- /dev/null +++ b/src/path-alias/spec/nested-aliases.out.ts @@ -0,0 +1,23 @@ +import { UserService } from "../src/services/user.ts"; +import { AuthService } from "../src/services/auth.ts"; +import { Logger } from "../src/utils/logger.ts"; +import { Database } from "../src/database/connection.ts"; +import { Config } from "../src/config/app.ts"; +import { ValidationError } from "../src/types/errors.ts"; +import { UserModel } from "../src/models/user.ts"; +import { AuthModel } from "../src/models/auth.ts"; + +export class App { + constructor( + private userService: UserService, + private authService: AuthService, + private logger: Logger, + private db: Database, + private config: Config, + ) {} + + async initialize() { + this.logger.info("Initializing application"); + await this.db.connect(); + } +} diff --git a/src/path-alias/spec/re-exports.in.ts b/src/path-alias/spec/re-exports.in.ts new file mode 100644 index 0000000..505db28 --- /dev/null +++ b/src/path-alias/spec/re-exports.in.ts @@ -0,0 +1,12 @@ +// Re-export from path aliases +export { UserService } from "@services/user"; +export { AuthService } from "@services/auth"; +export { Logger } from "@utils/logger"; + +// Re-export with renaming +export { UserService as UserAPI } from "@services/user"; +export { AuthService as AuthAPI } from "@services/auth"; + +// Re-export all +export * from "@services/user"; +export * from "@utils/logger"; diff --git a/src/path-alias/spec/re-exports.out.ts b/src/path-alias/spec/re-exports.out.ts new file mode 100644 index 0000000..a11c3bf --- /dev/null +++ b/src/path-alias/spec/re-exports.out.ts @@ -0,0 +1,12 @@ +// Re-export from path aliases +export { UserService } from "../src/services/user.ts"; +export { AuthService } from "../src/services/auth.ts"; +export { Logger } from "../src/utils/logger.ts"; + +// Re-export with renaming +export { UserService as UserAPI } from "../src/services/user.ts"; +export { AuthService as AuthAPI } from "../src/services/auth.ts"; + +// Re-export all +export * from "../src/services/user.ts"; +export * from "../src/utils/logger.ts"; diff --git a/src/path-alias/spec/relative-imports.in.ts b/src/path-alias/spec/relative-imports.in.ts new file mode 100644 index 0000000..a03a94b --- /dev/null +++ b/src/path-alias/spec/relative-imports.in.ts @@ -0,0 +1,18 @@ +import { UserService } from "@services/user"; +import { Logger } from "./logger"; +import { Config } from "../config/app"; +import { Utils } from "../../utils/helpers"; + +export class UserController { + constructor( + private userService: UserService, + private logger: Logger, + private config: Config, + private utils: Utils, + ) {} + + async getUser(id: string) { + this.logger.info(`Fetching user ${id}`); + return this.userService.findById(id); + } +} diff --git a/src/path-alias/spec/relative-imports.out.ts b/src/path-alias/spec/relative-imports.out.ts new file mode 100644 index 0000000..1cf9ead --- /dev/null +++ b/src/path-alias/spec/relative-imports.out.ts @@ -0,0 +1,18 @@ +import { UserService } from "../src/services/user.ts"; +import { Logger } from "./logger"; +import { Config } from "../config/app"; +import { Utils } from "../../utils/helpers"; + +export class UserController { + constructor( + private userService: UserService, + private logger: Logger, + private config: Config, + private utils: Utils, + ) {} + + async getUser(id: string) { + this.logger.info(`Fetching user ${id}`); + return this.userService.findById(id); + } +} diff --git a/src/path-alias/spec/simple-alias.in.ts b/src/path-alias/spec/simple-alias.in.ts new file mode 100644 index 0000000..9990196 --- /dev/null +++ b/src/path-alias/spec/simple-alias.in.ts @@ -0,0 +1,16 @@ +import { UserService } from "@services/user"; +import { Logger } from "@utils/logger"; +import { Config } from "@config/app"; + +export class UserController { + constructor( + private userService: UserService, + private logger: Logger, + private config: Config, + ) {} + + async getUser(id: string) { + this.logger.info(`Fetching user ${id}`); + return this.userService.findById(id); + } +} diff --git a/src/path-alias/spec/simple-alias.out.ts b/src/path-alias/spec/simple-alias.out.ts new file mode 100644 index 0000000..ba9bdb9 --- /dev/null +++ b/src/path-alias/spec/simple-alias.out.ts @@ -0,0 +1,16 @@ +import { UserService } from "../src/path-alias/spec/src/services/user.ts"; +import { Logger } from "../src/path-alias/spec/src/utils/logger.ts"; +import { Config } from "../src/path-alias/spec/src/config/app.ts"; + +export class UserController { + constructor( + private userService: UserService, + private logger: Logger, + private config: Config, + ) {} + + async getUser(id: string) { + this.logger.info(`Fetching user ${id}`); + return this.userService.findById(id); + } +} diff --git a/src/path-alias/spec/src/config/app.ts b/src/path-alias/spec/src/config/app.ts new file mode 100644 index 0000000..dd6e52a --- /dev/null +++ b/src/path-alias/spec/src/config/app.ts @@ -0,0 +1,6 @@ +export class Config { + apiUrl = "http://localhost:3000"; + port = 3000; +} + +export default Config; diff --git a/src/path-alias/spec/src/config/database.ts b/src/path-alias/spec/src/config/database.ts new file mode 100644 index 0000000..3307ee2 --- /dev/null +++ b/src/path-alias/spec/src/config/database.ts @@ -0,0 +1,5 @@ +export class DatabaseConfig { + host = "localhost"; + port = 5432; + database = "test"; +} diff --git a/src/path-alias/spec/src/config/types.ts b/src/path-alias/spec/src/config/types.ts new file mode 100644 index 0000000..34ff5be --- /dev/null +++ b/src/path-alias/spec/src/config/types.ts @@ -0,0 +1,4 @@ +export type ConfigType = { + apiUrl: string; + port: number; +}; diff --git a/src/path-alias/spec/src/controllers/user.ts b/src/path-alias/spec/src/controllers/user.ts new file mode 100644 index 0000000..e9f16a3 --- /dev/null +++ b/src/path-alias/spec/src/controllers/user.ts @@ -0,0 +1,7 @@ +export default class UserController { + constructor() {} + + async getUser(id: string) { + return { id, name: "Test User" }; + } +} diff --git a/src/path-alias/spec/src/database/connection.ts b/src/path-alias/spec/src/database/connection.ts new file mode 100644 index 0000000..c63eadc --- /dev/null +++ b/src/path-alias/spec/src/database/connection.ts @@ -0,0 +1,5 @@ +export class Database { + async connect() { + return "connected"; + } +} diff --git a/src/path-alias/spec/src/database/types.ts b/src/path-alias/spec/src/database/types.ts new file mode 100644 index 0000000..ef90a92 --- /dev/null +++ b/src/path-alias/spec/src/database/types.ts @@ -0,0 +1,5 @@ +export type DatabaseType = { + host: string; + port: number; + database: string; +}; diff --git a/src/path-alias/spec/src/models/auth.ts b/src/path-alias/spec/src/models/auth.ts new file mode 100644 index 0000000..eb152ee --- /dev/null +++ b/src/path-alias/spec/src/models/auth.ts @@ -0,0 +1,9 @@ +export class AuthModel { + token: string; + userId: string; + + constructor(data: any) { + this.token = data.token; + this.userId = data.userId; + } +} diff --git a/src/path-alias/spec/src/models/index.ts b/src/path-alias/spec/src/models/index.ts new file mode 100644 index 0000000..fe31ed3 --- /dev/null +++ b/src/path-alias/spec/src/models/index.ts @@ -0,0 +1,6 @@ +export namespace UserModel { + export interface LoginCredentials { + username: string; + password: string; + } +} diff --git a/src/path-alias/spec/src/models/user.ts b/src/path-alias/spec/src/models/user.ts new file mode 100644 index 0000000..badae64 --- /dev/null +++ b/src/path-alias/spec/src/models/user.ts @@ -0,0 +1,11 @@ +export class UserModel { + id: string; + name: string; + email: string; + + constructor(data: any) { + this.id = data.id; + this.name = data.name; + this.email = data.email; + } +} diff --git a/src/path-alias/spec/src/modules/user.ts b/src/path-alias/spec/src/modules/user.ts new file mode 100644 index 0000000..836fcca --- /dev/null +++ b/src/path-alias/spec/src/modules/user.ts @@ -0,0 +1,7 @@ +export default class UserModule { + constructor() {} + + async getUser(id: string) { + return { id, name: "Test User" }; + } +} diff --git a/src/path-alias/spec/src/services/auth.ts b/src/path-alias/spec/src/services/auth.ts new file mode 100644 index 0000000..0efa0e4 --- /dev/null +++ b/src/path-alias/spec/src/services/auth.ts @@ -0,0 +1,13 @@ +export class AuthService { + async authenticate(credentials: any) { + return { token: "test-token" }; + } + + async initialize() { + return "initialized"; + } +} + +export class AuthConfig { + apiUrl = "http://localhost:3000"; +} diff --git a/src/path-alias/spec/src/services/user.ts b/src/path-alias/spec/src/services/user.ts new file mode 100644 index 0000000..0f15f85 --- /dev/null +++ b/src/path-alias/spec/src/services/user.ts @@ -0,0 +1,17 @@ +export class UserService { + async findById(id: string) { + return { id, name: "Test User" }; + } + + async create(userData: any) { + return { ...userData, id: "123" }; + } + + test() { + return "test"; + } + + getConfig() { + return { apiUrl: "http://localhost:3000" }; + } +} diff --git a/src/path-alias/spec/src/types/errors.ts b/src/path-alias/spec/src/types/errors.ts new file mode 100644 index 0000000..6519caf --- /dev/null +++ b/src/path-alias/spec/src/types/errors.ts @@ -0,0 +1,6 @@ +export class ValidationError extends Error { + constructor(message: string) { + super(message); + this.name = "ValidationError"; + } +} diff --git a/src/path-alias/spec/src/types/user.ts b/src/path-alias/spec/src/types/user.ts new file mode 100644 index 0000000..87f3436 --- /dev/null +++ b/src/path-alias/spec/src/types/user.ts @@ -0,0 +1,5 @@ +export type UserType = { + id: string; + name: string; + email: string; +}; diff --git a/src/path-alias/spec/src/utils/logger.ts b/src/path-alias/spec/src/utils/logger.ts new file mode 100644 index 0000000..59294d8 --- /dev/null +++ b/src/path-alias/spec/src/utils/logger.ts @@ -0,0 +1,17 @@ +export class Logger { + info(message: string) { + console.log(message); + } + + log(message: string, level: LogLevel) { + console.log(`[${level}] ${message}`); + } +} + +export enum LogLevel { + INFO = "INFO", + WARN = "WARN", + ERROR = "ERROR", +} + +export default Logger; diff --git a/src/path-alias/spec/tsconfig.json b/src/path-alias/spec/tsconfig.json new file mode 100644 index 0000000..bf847a8 --- /dev/null +++ b/src/path-alias/spec/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@services/*": ["src/services/*"], + "@utils/*": ["src/utils/*"], + "@config/*": ["src/config/*"], + "@database/*": ["src/database/*"], + "@types/*": ["src/types/*"], + "@models/*": ["src/models/*"], + "@controllers/*": ["src/controllers/*"], + "@modules/*": ["src/modules/*"] + } + } +} diff --git a/src/path-alias/spec/type-imports.in.ts b/src/path-alias/spec/type-imports.in.ts new file mode 100644 index 0000000..2ec829f --- /dev/null +++ b/src/path-alias/spec/type-imports.in.ts @@ -0,0 +1,17 @@ +import type { UserType } from "@types/user"; +import type { ConfigType } from "@config/types"; +import type { DatabaseType } from "@database/types"; + +import { UserService } from "@services/user"; + +export class UserController { + constructor(private userService: UserService) {} + + async createUser(userData: UserType): Promise { + return this.userService.create(userData); + } + + getConfig(): ConfigType { + return this.userService.getConfig(); + } +} diff --git a/src/path-alias/spec/type-imports.out.ts b/src/path-alias/spec/type-imports.out.ts new file mode 100644 index 0000000..16e3385 --- /dev/null +++ b/src/path-alias/spec/type-imports.out.ts @@ -0,0 +1,17 @@ +import type { UserType } from "../src/types/user.ts"; +import type { ConfigType } from "../src/config/types.ts"; +import type { DatabaseType } from "../src/database/types.ts"; + +import { UserService } from "../src/services/user.ts"; + +export class UserController { + constructor(private userService: UserService) {} + + async createUser(userData: UserType): Promise { + return this.userService.create(userData); + } + + getConfig(): ConfigType { + return this.userService.getConfig(); + } +} diff --git a/src/path-alias/spec/wildcard-alias.in.ts b/src/path-alias/spec/wildcard-alias.in.ts new file mode 100644 index 0000000..ed8ee97 --- /dev/null +++ b/src/path-alias/spec/wildcard-alias.in.ts @@ -0,0 +1,19 @@ +import { UserModel } from "@models/*"; +import { AuthService } from "@services/auth"; +import { DatabaseConfig } from "@config/database"; +import { ValidationError } from "@types/errors"; + +export class AuthController { + constructor( + private authService: AuthService, + private dbConfig: DatabaseConfig, + ) {} + + async login(credentials: UserModel.LoginCredentials) { + try { + return await this.authService.authenticate(credentials); + } catch (error) { + throw new ValidationError("Invalid credentials"); + } + } +} diff --git a/src/path-alias/spec/wildcard-alias.out.ts b/src/path-alias/spec/wildcard-alias.out.ts new file mode 100644 index 0000000..8aac299 --- /dev/null +++ b/src/path-alias/spec/wildcard-alias.out.ts @@ -0,0 +1,19 @@ +import { UserModel } from "../src/models/index.ts"; +import { AuthService } from "../src/services/auth.ts"; +import { DatabaseConfig } from "../src/config/database.ts"; +import { ValidationError } from "../src/types/errors.ts"; + +export class AuthController { + constructor( + private authService: AuthService, + private dbConfig: DatabaseConfig, + ) {} + + async login(credentials: UserModel.LoginCredentials) { + try { + return await this.authService.authenticate(credentials); + } catch (error) { + throw new ValidationError("Invalid credentials"); + } + } +} diff --git a/src/utils/test.ts b/src/utils/test.ts index 5400d53..9e0438d 100644 --- a/src/utils/test.ts +++ b/src/utils/test.ts @@ -1,5 +1,6 @@ import { assertEquals } from "@std/assert/equals"; import { exists } from "@std/fs/exists"; +import { join } from "@std/path/join"; import { spawn } from "node:child_process"; /** @@ -85,11 +86,15 @@ class TestUtils { } } -export const executeTest = async (codemod: (filePath: string) => void) => { +export const executeTest = async ( + dir: string, + codemod: (filePath: string) => void, +) => { + const tmpDir = join(dir, "spec/tmp"); const utils = new TestUtils(codemod); - const tmpExists = await exists("./tmp"); + const tmpExists = await exists(tmpDir); if (!tmpExists) { - await Deno.mkdir("./tmp"); + await Deno.mkdir(tmpDir); } return async ( @@ -100,7 +105,7 @@ export const executeTest = async (codemod: (filePath: string) => void) => { }, ) => { const { testFile, inFile, outFile } = config; - const tmpPath = `./tmp/${testFile}`; + const tmpPath = `${tmpDir}/${testFile}`; try { const originalContent = await Deno.readTextFile(