diff --git a/src/linter/linter.ts b/src/linter/linter.ts index 5947518c..14254887 100644 --- a/src/linter/linter.ts +++ b/src/linter/linter.ts @@ -6,7 +6,7 @@ import {taskStart} from "../utils/perf.js"; import path from "node:path"; import posixPath from "node:path/posix"; import {stat} from "node:fs/promises"; -import {ProjectGraph} from "@ui5/project"; +import {Project, ProjectGraph} from "@ui5/project"; import type {AbstractReader, Resource} from "@ui5/fs"; import ConfigManager, {UI5LintConfigType} from "../utils/ConfigManager.js"; import {Minimatch} from "minimatch"; @@ -29,12 +29,32 @@ export async function lintProject({ projectGraphDone(); let virBasePath = "/resources/"; + let fsBasePath, namespace; + if (project.getType() === "module") { + // For now, we only support one path mapping. + // This is mainly an internal limitation that requires a potential larger refactoring of the linter code. + const firstMapping = getFirstModulePathMapping(project); + if (firstMapping?.virBasePath?.startsWith("/resources/") && firstMapping?.fsBasePath) { + fsBasePath = firstMapping.fsBasePath; + if (firstMapping.virBasePath.endsWith("/")) { + // Cut off trailing slash + firstMapping.virBasePath = firstMapping.virBasePath.slice(0, -1); + } + namespace = firstMapping.virBasePath.substring("/resources/".length); + } else { + throw new Error("No paths configuration found in project with type ''module''"); + } + } else { + fsBasePath = project.getSourcePath(); + namespace = project.getNamespace(); + } + if (!project._isSourceNamespaced) { // Ensure the virtual filesystem includes the project namespace to allow relative imports // of framework resources from the project - virBasePath += project.getNamespace() + "/"; + virBasePath += namespace + "/"; } - const fsBasePath = project.getSourcePath(); + let reader = createReader({ fsBasePath, virBasePath, @@ -47,7 +67,7 @@ export async function lintProject({ if (!project._isSourceNamespaced) { // Dynamically add namespace if the physical project structure does not include it // This logic is identical to the specification implementation in ui5-project - virBasePathTest += project.getNamespace() + "/"; + virBasePathTest += namespace + "/"; } reader = createReaderCollection({ readers: [reader, createReader({ @@ -62,7 +82,7 @@ export async function lintProject({ const res = await lint(reader, { rootDir, - namespace: project.getNamespace(), + namespace, filePatterns, ignorePatterns, coverage, @@ -384,3 +404,14 @@ export function mergeIgnorePatterns(options: LinterOptions, config: UI5LintConfi ...(options.ignorePatterns ?? []), // CLI patterns go after config patterns ].filter(($) => $); } + +function getFirstModulePathMapping(project: Project) { + const pathMapping = project._config?.resources?.configuration?.paths; + if (pathMapping && Object.keys(pathMapping).length > 0) { + const virBasePath = Object.keys(pathMapping)[0]; + const fsBasePath = pathMapping[virBasePath]; + return {virBasePath, fsBasePath}; + } else { + return null; + } +} diff --git a/src/untyped.d.ts b/src/untyped.d.ts index 058e198f..657ebede 100644 --- a/src/untyped.d.ts +++ b/src/untyped.d.ts @@ -3,18 +3,32 @@ declare module "@ui5/project" { type ProjectNamespace = string; + + // Note: This is only a partial definition with required fields for type module + interface ProjectConfig { + resources: { + configuration: { + paths: Record; + }; + }; + } + interface Project { - getNamespace: () => ProjectNamespace; - getReader: (options: import("@ui5/fs").ReaderOptions) => import("@ui5/fs").AbstractReader; - getRootReader: () => import("@ui5/fs").AbstractReader; - getRootPath: () => string; - getSourcePath: () => string; - _testPath: string; // TODO UI5 Tooling: Expose API for optional test path + getNamespace(): ProjectNamespace; + getReader(options: import("@ui5/fs").ReaderOptions): import("@ui5/fs").AbstractReader; + getRootReader(): import("@ui5/fs").AbstractReader; + getRootPath(): string; + getSourcePath(): string; + getType(): "application" | "library" | "module"; + + // TODO UI5 Tooling: Expose required information as API + _testPath: string; _testPathExists: string; _isSourceNamespaced: boolean; + _config: ProjectConfig; // Needed to read the paths configuration of projects with type module } interface ProjectGraph { - getRoot: () => Project; + getRoot(): Project; } interface DependencyTreeNode { id: string; @@ -70,13 +84,13 @@ declare module "@ui5/fs" { contentModified: boolean; } interface Resource { - getBuffer: () => Promise; - getString: () => Promise; - getStream: () => import("node:fs").ReadStream; - getName: () => string; - getPath: () => ResourcePath; - getProject: () => import("@ui5/project").Project; - getSourceMetadata: () => ResourceSourceMetadata; + getBuffer(): Promise; + getString(): Promise; + getStream(): import("node:fs").ReadStream; + getName(): string; + getPath(): ResourcePath; + getProject(): import("@ui5/project").Project; + getSourceMetadata(): ResourceSourceMetadata; } type ReaderStyles = "buildtime" | "dist" | "runtime" | "flat"; @@ -89,11 +103,11 @@ declare module "@ui5/fs" { type Filter = (resource: Resource) => boolean; export interface AbstractReader { - byGlob: (virPattern: string | string[], options?: GlobOptions) => Promise; - byPath: (path: string) => Promise; + byGlob(virPattern: string | string[], options?: GlobOptions): Promise; + byPath(path: string): Promise; } export interface AbstractAdapter extends AbstractReader { - write: (resource: Resource) => Promise; + write(resource: Resource): Promise; } } @@ -133,13 +147,13 @@ declare module "@ui5/fs/resourceFactory" { declare module "@ui5/logger" { interface Logger { - silly: (message: string) => void; - verbose: (message: string) => void; - perf: (message: string) => void; - info: (message: string) => void; - warn: (message: string) => void; - error: (message: string) => void; - isLevelEnabled: (level: string) => boolean; + silly(message: string): void; + verbose(message: string): void; + perf(message: string): void; + info(message: string): void; + warn(message: string): void; + error(message: string): void; + isLevelEnabled(level: string): boolean; } export function isLogLevelEnabled(logLevel: string): boolean;