diff --git a/Sources/TypeScriptAST/Component/PathPrefixReplacement.swift b/Sources/TypeScriptAST/Component/PathPrefixReplacement.swift new file mode 100644 index 0000000..7531943 --- /dev/null +++ b/Sources/TypeScriptAST/Component/PathPrefixReplacement.swift @@ -0,0 +1,41 @@ +import Foundation + +public struct PathPrefixReplacement { + public init( + path: URL, + replacement: String + ) { + self.path = path + self.replacement = replacement + } + + public var path: URL + public var replacement: String + + public func replace(path: URL) -> URL? { + let base = self.path.absoluteURL.standardized.pathComponents + let full = path.absoluteURL.standardized.pathComponents + + guard full.starts(with: base) else { + return nil + } + + let delta = full[base.count...] + + return URL(fileURLWithPath: replacement) + .appendingPathComponent(delta.joined(separator: "/")) + } +} + +public typealias PathPrefixReplacements = [PathPrefixReplacement] + +extension PathPrefixReplacements { + public func replace(path: URL) -> URL? { + for x in self { + if let new = x.replace(path: path) { + return new + } + } + return nil + } +} diff --git a/Sources/TypeScriptAST/Dependency/AutoImport.swift b/Sources/TypeScriptAST/Dependency/AutoImport.swift index a57f5fa..7fde83a 100644 --- a/Sources/TypeScriptAST/Dependency/AutoImport.swift +++ b/Sources/TypeScriptAST/Dependency/AutoImport.swift @@ -16,6 +16,7 @@ extension TSSourceFile { from: URL, symbolTable: SymbolTable, fileExtension: ImportFileExtension, + pathPrefixReplacements: PathPrefixReplacements = [], defaultFile: String? = nil ) throws -> [TSImportDecl] { var fileToSymbols = FileToSymbols() @@ -65,6 +66,14 @@ extension TSSourceFile { let symbols = Set(fileToSymbols.symbols(for: file)).sorted() if symbols.isEmpty { continue } + var file = file + + if let newPath = pathPrefixReplacements.replace( + path: URL(fileURLWithPath: file, relativeTo: from) + ) { + file = newPath.relativePath + } + imports.append( TSImportDecl(names: symbols, from: file) ) diff --git a/Tests/TypeScriptASTTests/AutoImportTests.swift b/Tests/TypeScriptASTTests/AutoImportTests.swift index eaaf776..4a29175 100644 --- a/Tests/TypeScriptASTTests/AutoImportTests.swift +++ b/Tests/TypeScriptASTTests/AutoImportTests.swift @@ -196,6 +196,71 @@ final class AutoImportTests: TestCaseBase { ) } + func testAlias() throws { + let s = TSSourceFile([ + TSTypeDecl(modifiers: [.export], name: "S", type: TSObjectType([])) + ]) + + let m = TSSourceFile([ + TSTypeDecl(modifiers: [.export], name: "M", type: TSIdentType("S")) + ]) + + var symbols = SymbolTable() + symbols.add(source: s, file: URL(fileURLWithPath: "lib/foo/s.ts")) + + let imports = try m.buildAutoImportDecls( + from: URL(fileURLWithPath: "m/m.ts"), + symbolTable: symbols, + fileExtension: .none, + pathPrefixReplacements: [ + .init(path: URL(fileURLWithPath: "lib/foo"), replacement: "@foo") + ] + ) + m.replaceImportDecls(imports) + + assertPrint( + m, """ + import { S } from "@foo/s"; + + export type M = S; + + """ + ) + } + + func testUnrelatedAlias() throws { + let s = TSSourceFile([ + TSTypeDecl(modifiers: [.export], name: "S", type: TSObjectType([])) + ]) + + let m = TSSourceFile([ + TSTypeDecl(modifiers: [.export], name: "M", type: TSIdentType("S")) + ]) + + var symbols = SymbolTable() + symbols.add(source: s, file: URL(fileURLWithPath: "s/s.ts")) + + let imports = try m.buildAutoImportDecls( + from: URL(fileURLWithPath: "m/m.ts"), + symbolTable: symbols, + fileExtension: .none, + pathPrefixReplacements: [ + .init(path: URL(fileURLWithPath: "lib/foo"), replacement: "@foo") + ] + ) + m.replaceImportDecls(imports) + + assertPrint( + m, """ + import { S } from "../s/s"; + + export type M = S; + + """ + ) + } + + func testDefaultImport() throws { let s = TSSourceFile([ TSVarDecl( diff --git a/Tests/TypeScriptASTTests/UtilsTests.swift b/Tests/TypeScriptASTTests/UtilsTests.swift index 8098610..d7b42c6 100644 --- a/Tests/TypeScriptASTTests/UtilsTests.swift +++ b/Tests/TypeScriptASTTests/UtilsTests.swift @@ -40,4 +40,34 @@ final class UtilsTests: XCTestCase { XCTAssertNil(u.baseURL) } } + + func testPathPrefixReplace() throws { + let args: [(from: String, to: String, input: String, expect: String?, line: UInt)] = [ + // full to full + ("/usr", "/foo", "/usr/lib", "/foo/lib", #line), + // full to rel + ("/usr", "foo", "/usr/lib", "foo/lib", #line), + ("/usr", "../foo", "/usr/lib", "../foo/lib", #line), + // rel to full + ("usr", "/foo", "usr/lib", "/foo/lib", #line), + ("../usr", "/foo", "../usr/lib", "/foo/lib", #line), + // rel to rel + ("usr", "foo", "usr/lib", "foo/lib", #line), + ("../usr", "foo", "../usr/lib", "foo/lib", #line), + // slash + ("/usr", "/foo", "/usr2/lib", nil, #line), + ] + + for arg in args { + let replacement = PathPrefixReplacement( + path: URL(fileURLWithPath: arg.from), replacement: arg.to + ) + let input = URL(fileURLWithPath: arg.input) + let expect = arg.expect.map { URL(fileURLWithPath: $0) } + + let actual = replacement.replace(path: input) + + XCTAssertEqual(actual, expect, file: #file, line: arg.line) + } + } }