From aa090d9f04db7c47616ad180036b36983c89e2e1 Mon Sep 17 00:00:00 2001 From: Frank Weigel Date: Wed, 29 Jan 2025 17:09:21 +0100 Subject: [PATCH] fix: Resolve relative dependencies before creating imports from them TypeScript cannot recognize that a relative dependency "./library" in a module "sap/ui/unified/ColorPicker" maps to the ambient module "sap/ui/unified/library" (without import maps / package.json entries, relative import specifiers don't map to any bare modules). By resolving relative dependencies before creating imports from them, this can be improved. As the resolution is no longer unique when "resource-roots" configuration is used, the resolution is limited to productive code. in test code, the use of "resource-roots" mappings is much more likely. --- .../moduleDeclarationToDefinition.ts | 30 +++++++++++++++++-- .../ui5Types/amdTranspiler/tsTransformer.ts | 11 ++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/linter/ui5Types/amdTranspiler/moduleDeclarationToDefinition.ts b/src/linter/ui5Types/amdTranspiler/moduleDeclarationToDefinition.ts index aae78b67e..1caaffa94 100644 --- a/src/linter/ui5Types/amdTranspiler/moduleDeclarationToDefinition.ts +++ b/src/linter/ui5Types/amdTranspiler/moduleDeclarationToDefinition.ts @@ -1,4 +1,5 @@ import ts from "typescript"; +import * as path from "node:path"; import {getLogger} from "@ui5/logger"; import {ModuleDeclaration} from "./parseModuleDeclaration.js"; import rewriteExtendCall, {UnsupportedExtendCall} from "./rewriteExtendCall.js"; @@ -17,9 +18,10 @@ export interface ModuleDefinition { } export default function ( - moduleDeclaration: ModuleDeclaration, sourceFile: ts.SourceFile, nodeFactory: ts.NodeFactory + moduleDeclaration: ModuleDeclaration, sourceFile: ts.SourceFile, nodeFactory: ts.NodeFactory, + defaultModuleID?: string ): ModuleDefinition { - const {imports, identifiers: importIdentifiers} = collectImports(moduleDeclaration, nodeFactory); + const {imports, identifiers: importIdentifiers} = collectImports(moduleDeclaration, nodeFactory, defaultModuleID); const {body, oldFactoryBlock, moveComments} = getModuleBody(moduleDeclaration, sourceFile, nodeFactory, importIdentifiers); /* Ignore module name and export flag for now */ @@ -40,10 +42,13 @@ export default function ( */ function collectImports( moduleDeclaration: ModuleDeclaration, - nodeFactory: ts.NodeFactory + nodeFactory: ts.NodeFactory, + defaultModuleID: string | undefined ): {imports: ts.ImportDeclaration[]; identifiers: ts.Identifier[]} { const imports: ts.ImportDeclaration[] = []; const identifiers: ts.Identifier[] = []; + const hasRelativeSegment = /(^|\/)\.{1,2}\//; + if (!moduleDeclaration.dependencies) { // No dependencies declared return {imports, identifiers}; @@ -91,6 +96,25 @@ function collectImports( } else { moduleSpecifier = dep; } + if (hasRelativeSegment.test(moduleSpecifier.text)) { + const baseModuleID = moduleDeclaration.moduleName?.text ?? defaultModuleID; + if (baseModuleID == null) { + log.verbose(`Skipping resolution of relative moduleID ${moduleSpecifier.text} (no base known) at ` + + toPosStr(dep)); + } else { + const resolvedModuleID = + path.posix.resolve( + path.posix.dirname("/" + baseModuleID), + moduleSpecifier.text).slice(1); + log.silly(`Resolved relative module ID ${moduleSpecifier.text} to ${resolvedModuleID}`); + const resolvedModuleSpecifier = nodeFactory.createStringLiteral( + resolvedModuleID + ); + // Set range to the original range to preserve source mapping capability + ts.setTextRange(resolvedModuleSpecifier, moduleSpecifier); + moduleSpecifier = resolvedModuleSpecifier; + } + } let identifier: ts.Identifier | undefined; if (factoryRequiresCallWrapper) { // Generate variable name based on import module diff --git a/src/linter/ui5Types/amdTranspiler/tsTransformer.ts b/src/linter/ui5Types/amdTranspiler/tsTransformer.ts index a0726a192..1a8ee4600 100644 --- a/src/linter/ui5Types/amdTranspiler/tsTransformer.ts +++ b/src/linter/ui5Types/amdTranspiler/tsTransformer.ts @@ -103,6 +103,14 @@ function transform( const metadata = context.getMetadata(resourcePath); findDirectives(sourceFile, metadata); + // For now, only resolve relative imoprts in productive code. + // (In test code, issues with externally configured resource roots are + // more likely) + const defaultModuleID = + resourcePath.startsWith("/resources/") ? + resourcePath.slice("/resources/".length, -2) : // extract runtime module ID + undefined; + // Visit the AST depth-first and collect module definitions function visit(nodeIn: ts.Node): ts.VisitResult { const node = ts.visitEachChild(nodeIn, visit, tContext); @@ -111,7 +119,8 @@ function transform( if (matchPropertyAccessExpression(node.expression, "sap.ui.define")) { try { const moduleDeclaration = parseModuleDeclaration(node.arguments, checker); - const moduleDefinition = moduleDeclarationToDefinition(moduleDeclaration, sourceFile, nodeFactory); + const moduleDefinition = moduleDeclarationToDefinition(moduleDeclaration, sourceFile, + nodeFactory, defaultModuleID); moduleDefinitions.push(moduleDefinition); if (moduleDefinition.imports.length) { moduleDefinition.imports.forEach((importStatement) =>