From 717c015cc3698e6c3e5f50b203cbbe4be4662d29 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 28 Aug 2024 17:20:13 +0200 Subject: [PATCH] [INTERNAL] TypeScript: Add typings to BuildContext, Specification, ProjectGraph --- src/build/helpers/BuildContext.ts | 27 +++++- src/build/helpers/ProjectBuildContext.ts | 18 +++- src/build/helpers/TaskUtil.ts | 12 +-- src/build/helpers/createBuildManifest.ts | 2 +- src/graph/ProjectGraph.ts | 104 ++++++++++++++--------- src/specifications/Project.ts | 1 - src/specifications/Specification.ts | 88 ++++++++++++------- 7 files changed, 166 insertions(+), 86 deletions(-) diff --git a/src/build/helpers/BuildContext.ts b/src/build/helpers/BuildContext.ts index 95c014ab2..28bd4c555 100644 --- a/src/build/helpers/BuildContext.ts +++ b/src/build/helpers/BuildContext.ts @@ -1,19 +1,38 @@ +import type ProjectGraph from "../../graph/ProjectGraph.js"; +import type Project from "../../specifications/Project.js"; import ProjectBuildContext from "./ProjectBuildContext.js"; import OutputStyleEnum from "./ProjectBuilderOutputStyle.js"; +import type * as taskRepositoryModule from "@ui5/builder/internal/taskRepository"; + +interface BuildConfig { + selfContained: boolean; + cssVariables: boolean; + jsdoc: boolean; + createBuildManifest: boolean; + outputStyle: typeof OutputStyleEnum[keyof typeof OutputStyleEnum]; + includedTasks: string[]; + excludedTasks: string[]; +} /** * Context of a build process * */ class BuildContext { - constructor(graph, taskRepository, { // buildConfig + _graph: ProjectGraph; + _buildConfig: BuildConfig; + _taskRepository: typeof taskRepositoryModule; + _options: {cssVariables: boolean}; + _projectBuildContexts: ProjectBuildContext[]; + + constructor(graph: ProjectGraph, taskRepository: typeof taskRepositoryModule, { // buildConfig selfContained = false, cssVariables = false, jsdoc = false, createBuildManifest = false, outputStyle = OutputStyleEnum.Default, includedTasks = [], excludedTasks = [], - } = {}) { + } = {} as Partial) { if (!graph) { throw new Error(`Missing parameter 'graph'`); } @@ -79,7 +98,7 @@ class BuildContext { return this._graph.getRoot(); } - getOption(key) { + getOption(key: keyof typeof this._options) { return this._options[key]; } @@ -95,7 +114,7 @@ class BuildContext { return this._graph; } - createProjectContext({project}) { + createProjectContext({project}: {project: Project}) { const projectBuildContext = new ProjectBuildContext({ buildContext: this, project, diff --git a/src/build/helpers/ProjectBuildContext.ts b/src/build/helpers/ProjectBuildContext.ts index 06ffc4f30..1e22ee082 100644 --- a/src/build/helpers/ProjectBuildContext.ts +++ b/src/build/helpers/ProjectBuildContext.ts @@ -2,6 +2,10 @@ import ResourceTagCollection from "@ui5/fs/internal/ResourceTagCollection"; import ProjectBuildLogger from "@ui5/logger/internal/loggers/ProjectBuild"; import TaskUtil from "./TaskUtil.js"; import TaskRunner from "../TaskRunner.js"; +import type BuildContext from "./BuildContext.js"; +import type Project from "../../specifications/Project.js"; + +export type CleanupCallback = (force: boolean) => Promise; /** * Build context of a single project. Always part of an overall @@ -9,7 +13,13 @@ import TaskRunner from "../TaskRunner.js"; * */ class ProjectBuildContext { - constructor({buildContext, project}) { + _buildContext: BuildContext; + _project: Project; + _log: ProjectBuildLogger; + _queues: {cleanup: CleanupCallback[]}; + _resourceTagCollection: ResourceTagCollection; + + constructor({buildContext, project}: {buildContext: BuildContext; project: Project}) { if (!buildContext) { throw new Error(`Missing parameter 'buildContext'`); } @@ -37,15 +47,15 @@ class ProjectBuildContext { return this._project === this._buildContext.getRootProject(); } - getOption(key) { + getOption(key: keyof typeof BuildContext.prototype._options) { return this._buildContext.getOption(key); } - registerCleanupTask(callback) { + registerCleanupTask(callback: CleanupCallback) { this._queues.cleanup.push(callback); } - async executeCleanupTasks(force) { + async executeCleanupTasks(force: boolean) { await Promise.all(this._queues.cleanup.map((callback) => { return callback(force); })); diff --git a/src/build/helpers/TaskUtil.ts b/src/build/helpers/TaskUtil.ts index 44ffdce0f..076c07809 100644 --- a/src/build/helpers/TaskUtil.ts +++ b/src/build/helpers/TaskUtil.ts @@ -1,3 +1,4 @@ +import {ResourceInterface} from "@ui5/fs/Resource"; import { createReaderCollection, createReaderCollectionPrioritized, @@ -16,7 +17,6 @@ import { * The set of available functions on that interface depends on the specification * version defined for the extension. * - * @alias @ui5/project/build/helpers/TaskUtil * @hideconstructor */ class TaskUtil { @@ -47,7 +47,9 @@ class TaskUtil { * @param parameters * @param parameters.projectBuildContext ProjectBuildContext */ - constructor({projectBuildContext}: object) { + STANDARD_TAGS: object; + + constructor({projectBuildContext}) { this._projectBuildContext = projectBuildContext; /** */ @@ -79,7 +81,7 @@ class TaskUtil { * [STANDARD_TAGS]{@link @ui5/project/build/helpers/TaskUtil#STANDARD_TAGS} are allowed * @param [value] Tag value. Must be primitive */ - public setTag(resource, tag: string, value?: string | boolean | integer) { + public setTag(resource: ResourceInterface, tag: string, value?: string | boolean | number) { if (typeof resource === "string") { throw new Error("Deprecated parameter: " + "Since UI5 Tooling 3.0, #setTag requires a resource instance. Strings are no longer accepted"); @@ -101,7 +103,7 @@ class TaskUtil { * @returns Tag value for the given resource. * undefined if no value is available */ - public getTag(resource, tag: string) { + public getTag(resource: ResourceInterface, tag: string) { if (typeof resource === "string") { throw new Error("Deprecated parameter: " + "Since UI5 Tooling 3.0, #getTag requires a resource instance. Strings are no longer accepted"); @@ -121,7 +123,7 @@ class TaskUtil { * @param resource Resource-instance the tag should be cleared for * @param tag Tag */ - public clearTag(resource, tag: string) { + public clearTag(resource: ResourceInterface, tag: string) { if (typeof resource === "string") { throw new Error("Deprecated parameter: " + "Since UI5 Tooling 3.0, #clearTag requires a resource instance. Strings are no longer accepted"); diff --git a/src/build/helpers/createBuildManifest.ts b/src/build/helpers/createBuildManifest.ts index 35a6324a4..c851da9b9 100644 --- a/src/build/helpers/createBuildManifest.ts +++ b/src/build/helpers/createBuildManifest.ts @@ -30,7 +30,7 @@ function getSortedTags(project) { * @param buildConfig * @param taskRepository */ -export default async function (project, buildConfig, taskRepository) { +export default async function (project, buildConfig, taskRepository: typeof import("@ui5/builder/internal/taskRepository")) { if (!project) { throw new Error(`Missing parameter 'project'`); } diff --git a/src/graph/ProjectGraph.ts b/src/graph/ProjectGraph.ts index 1dca99030..a836d5b29 100644 --- a/src/graph/ProjectGraph.ts +++ b/src/graph/ProjectGraph.ts @@ -1,15 +1,31 @@ import OutputStyleEnum from "../build/helpers/ProjectBuilderOutputStyle.js"; import {getLogger} from "@ui5/logger"; +import type Project from "../specifications/Project.js"; +import type Extension from "../specifications/Extension.js"; +import type * as taskRepositoryModule from "@ui5/builder/internal/taskRepository"; const log = getLogger("graph:ProjectGraph"); +type TraversalCallback = (arg: {project: Project; dependencies: string[]}) => Promise; +type VisitedNodes = Record | undefined>; + /** * A rooted, directed graph representing a UI5 project, its dependencies and available extensions. *

* While it allows defining cyclic dependencies, both traversal functions will throw an error if they encounter cycles. * - * @alias @ui5/project/graph/ProjectGraph */ class ProjectGraph { + _rootProjectName: string; + + _projects: Map; + _adjList: Map>; + _optAdjList: Map>; + _extensions: Map; + + _sealed: boolean; + _hasUnresolvedOptionalDependencies: boolean; + _taskRepository: typeof taskRepositoryModule | null; + /** * @param parameters Parameters * @param parameters.rootProjectName Root project name @@ -38,7 +54,7 @@ class ProjectGraph { * * @returns Root project */ - public getRoot() { + public getRoot(): Project { const rootProject = this._projects.get(this._rootProjectName); if (!rootProject) { throw new Error(`Unable to find root project with name ${this._rootProjectName} in project graph`); @@ -51,7 +67,7 @@ class ProjectGraph { * * @param project Project which should be added to the graph */ - public addProject(project) { + public addProject(project: Project) { this._checkSealed(); const projectName = project.getName(); if (this._projects.has(projectName)) { @@ -59,7 +75,7 @@ class ProjectGraph { `Failed to add project ${projectName} to graph: A project with that name has already been added. ` + `This might be caused by multiple modules containing projects with the same name`); } - if (!isNaN(projectName)) { + if (!isNaN(projectName as unknown as number)) { // Reject integer-like project names. They would take precedence when traversing object keys which // could lead to unexpected behavior. We don't really expect anyone to use such names anyways throw new Error( @@ -114,7 +130,7 @@ class ProjectGraph { * * @param extension Extension which should be available in the graph */ - public addExtension(extension) { + public addExtension(extension: Extension) { this._checkSealed(); const extensionName = extension.getName(); if (this._extensions.has(extensionName)) { @@ -123,7 +139,7 @@ class ProjectGraph { `An extension with that name has already been added. ` + `This might be caused by multiple modules containing extensions with the same name`); } - if (!isNaN(extensionName)) { + if (!isNaN(extensionName as unknown as number)) { // Reject integer-like extension names. They would take precedence when traversing object keys which // might lead to unexpected behavior in the future. We don't really expect anyone to use such names anyways throw new Error( @@ -171,9 +187,12 @@ class ProjectGraph { log.verbose(`Declaring dependency: ${fromProjectName} depends on ${toProjectName}`); this._declareDependency(this._adjList, fromProjectName, toProjectName); } catch (err) { - throw new Error( - `Failed to declare dependency from project ${fromProjectName} to ${toProjectName}: ` + - err.message); + if (err instanceof Error) { + throw new Error( + `Failed to declare dependency from project ${fromProjectName} to ${toProjectName}: ` + + err.message); + } + throw err; } } @@ -190,9 +209,12 @@ class ProjectGraph { this._declareDependency(this._optAdjList, fromProjectName, toProjectName); this._hasUnresolvedOptionalDependencies = true; } catch (err) { - throw new Error( - `Failed to declare optional dependency from project ${fromProjectName} to ${toProjectName}: ` + - err.message); + if (err instanceof Error) { + throw new Error( + `Failed to declare optional dependency from project ${fromProjectName} to ${toProjectName}: ` + + err.message); + } + throw err; } } @@ -203,7 +225,7 @@ class ProjectGraph { * @param fromProjectName Name of the depending project * @param toProjectName Name of project on which the other depends */ - _declareDependency(map: object, fromProjectName: string, toProjectName: string) { + _declareDependency(map: typeof this._adjList, fromProjectName: string, toProjectName: string) { if (!this._projects.has(fromProjectName)) { throw new Error( `Unable to find depending project with name ${fromProjectName} in project graph`); @@ -216,7 +238,7 @@ class ProjectGraph { throw new Error( `A project can't depend on itself`); } - const adjacencies = map.get(fromProjectName); + const adjacencies = map.get(fromProjectName)!; if (adjacencies.has(toProjectName)) { log.warn(`Dependency has already been declared: ${fromProjectName} depends on ${toProjectName}`); } else { @@ -254,8 +276,8 @@ class ProjectGraph { `Unable to find project in project graph`); } - const processDependency = (depName) => { - const adjacencies = this._adjList.get(depName); + const processDependency = (depName: string) => { + const adjacencies = this._adjList.get(depName)!; adjacencies.forEach((depName) => { if (!dependencies.has(depName)) { dependencies.add(depName); @@ -287,7 +309,7 @@ class ProjectGraph { if (adjacencies.has(toProjectName)) { return false; } - const optAdjacencies = this._optAdjList.get(fromProjectName); + const optAdjacencies = this._optAdjList.get(fromProjectName)!; if (optAdjacencies.has(toProjectName)) { return true; } @@ -340,26 +362,26 @@ class ProjectGraph { } } - public async traverseBreadthFirst(startName?: string, callback) { + public async traverseBreadthFirst(startName?: string, callback?: TraversalCallback) { if (!callback) { // Default optional first parameter - callback = startName; + callback = startName as unknown as TraversalCallback; startName = this._rootProjectName; } - if (!this.getProject(startName)) { + if (!this.getProject(startName!)) { throw new Error(`Failed to start graph traversal: Could not find project ${startName} in project graph`); } const queue = [{ - projectNames: [startName], - ancestors: [], + projectNames: [startName] as string[], + ancestors: [] as string[], }]; - const visited = Object.create(null); + const visited = Object.create(null) as VisitedNodes; while (queue.length) { - const {projectNames, ancestors} = queue.shift(); // Get and remove first entry from queue + const {projectNames, ancestors} = queue.shift()!; // Get and remove first entry from queue await Promise.all(projectNames.map(async (projectName) => { this._checkCycle(ancestors, projectName); @@ -386,20 +408,22 @@ class ProjectGraph { } } - public async traverseDepthFirst(startName?: string, callback) { + public async traverseDepthFirst(startName?: string, callback?: TraversalCallback) { if (!callback) { // Default optional first parameter - callback = startName; + callback = startName as unknown as TraversalCallback; startName = this._rootProjectName; } - if (!this.getProject(startName)) { + if (!this.getProject(startName!)) { throw new Error(`Failed to start graph traversal: Could not find project ${startName} in project graph`); } - return this._traverseDepthFirst(startName, Object.create(null), [], callback); + return this._traverseDepthFirst(startName!, Object.create(null) as VisitedNodes, [], callback); } - async _traverseDepthFirst(projectName, visited, ancestors, callback) { + async _traverseDepthFirst( + projectName: string, visited: VisitedNodes, ancestors: string[], callback: TraversalCallback + ) { this._checkCycle(ancestors, projectName); if (visited[projectName]) { @@ -425,7 +449,7 @@ class ProjectGraph { * * @param projectGraph Project Graph to merge into this one */ - public join(projectGraph) { + public join(projectGraph: ProjectGraph) { try { this._checkSealed(); if (!projectGraph.isSealed()) { @@ -450,7 +474,7 @@ class ProjectGraph { } // Only to be used by @ui5/builder tests to inject its version of the taskRepository - setTaskRepository(taskRepository) { + setTaskRepository(taskRepository: typeof taskRepositoryModule) { this._taskRepository = taskRepository; } @@ -459,9 +483,12 @@ class ProjectGraph { try { this._taskRepository = await import("@ui5/builder/internal/taskRepository"); } catch (err) { - throw new Error( - `Failed to load task repository. Missing dependency to '@ui5/builder'? ` + - `Error: ${err.message}`); + if (err instanceof Error) { + throw new Error( + `Failed to load task repository. Missing dependency to '@ui5/builder'? ` + + `Error: ${err.message}`); + } + throw err; } } return this._taskRepository; @@ -530,7 +557,7 @@ class ProjectGraph { } } - _checkCycle(ancestors, projectName) { + _checkCycle(ancestors: string[], projectName: string) { if (ancestors.includes(projectName)) { // "Back-edge" detected. Neither BFS nor DFS searches should continue // Mark first and last occurrence in chain with an asterisk and throw an error detailing the @@ -543,12 +570,7 @@ class ProjectGraph { // TODO: introduce function to check for dangling nodes/consistency in general? } -/** - * - * @param target - * @param source - */ -function mergeMap(target, source) { +function mergeMap(target: Map>, source: Map>) { for (const [key, value] of source) { if (target.has(key)) { throw new Error(`Failed to merge map: Key '${key}' already present in target set`); diff --git a/src/specifications/Project.ts b/src/specifications/Project.ts index cc46043a3..d1d74a399 100644 --- a/src/specifications/Project.ts +++ b/src/specifications/Project.ts @@ -4,7 +4,6 @@ import ResourceTagCollection from "@ui5/fs/internal/ResourceTagCollection"; /** * Project * - * @alias @ui5/project/specifications/Project * @hideconstructor */ class Project extends Specification { diff --git a/src/specifications/Specification.ts b/src/specifications/Specification.ts index 4a045fcf0..221a46422 100644 --- a/src/specifications/Specification.ts +++ b/src/specifications/Specification.ts @@ -1,21 +1,51 @@ import path from "node:path"; +import type Logger from "@ui5/logger/Logger"; import {getLogger} from "@ui5/logger"; import {createReader} from "@ui5/fs/resourceFactory"; import SpecificationVersion from "./SpecificationVersion.js"; +export interface SpecificationConfiguration { + kind: string; + type: string; + specVersion: string; + metadata: { + name: string; + }; +} + +interface LegacySpecificationConfiguration extends SpecificationConfiguration { + resources?: { + configuration?: { + propertiesFileSourceEncoding?: string; + }; + }; +} + +interface SpecificationParameters { + id: string; + version: string; + modulePath: string; + configuration: SpecificationConfiguration; +} + /** * Abstract superclass for all projects and extensions * - * @alias @ui5/project/specifications/Specification * @hideconstructor */ class Specification { - public static async create(parameters: { - id: string; - version: string; - modulePath: string; - configuration: object; - }) { + _log: Logger; + _version!: string; + _modulePath!: string; + __id!: string; + _name!: string; + _kind!: string; + _type!: string; + _specVersionString!: string; + _specVersion!: SpecificationVersion; + _config!: SpecificationConfiguration; + + public static async create(parameters: SpecificationParameters) { if (!parameters.configuration) { throw new Error( `Unable to create Specification instance: Missing configuration parameter`); @@ -99,7 +129,7 @@ class Specification { this.__id = id; // Deep clone config to prevent changes by reference - const config = JSON.parse(JSON.stringify(configuration)); + const config = JSON.parse(JSON.stringify(configuration)) as SpecificationConfiguration; const {validate} = await import("../validation/validator.js"); if (SpecificationVersion.major(config.specVersion) <= 1) { @@ -107,7 +137,7 @@ class Specification { this._log.verbose(`Detected legacy Specification Version ${config.specVersion}, defined for ` + `${config.kind} ${config.metadata.name}. ` + `Attempting to migrate the project to a supported specification version...`); - this._migrateLegacyProject(config); + this._migrateLegacyProject(config as LegacySpecificationConfiguration); try { await validate({ config, @@ -116,15 +146,18 @@ class Specification { }, }); } catch (err) { - this._log.verbose( - `Validation error after migration of ${config.kind} ${config.metadata.name}:`); - this._log.verbose(err.message); - throw new Error( - `${config.kind} ${config.metadata.name} defines unsupported Specification Version ` + - `${originalSpecVersion}. Please manually upgrade to 3.0 or higher. ` + - `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions - ` + - `An attempted migration to a supported specification version failed, ` + - `likely due to unrecognized configuration. Check verbose log for details.`); + if (err instanceof Error) { + this._log.verbose( + `Validation error after migration of ${config.kind} ${config.metadata.name}:`); + this._log.verbose(err.message); + throw new Error( + `${config.kind} ${config.metadata.name} defines unsupported Specification Version ` + + `${originalSpecVersion}. Please manually upgrade to 3.0 or higher. ` + + `For details see https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions - ` + + `An attempted migration to a supported specification version failed, ` + + `likely due to unrecognized configuration. Check verbose log for details.`); + } + throw err; } } else { await validate({ @@ -235,7 +268,7 @@ class Specification { * @returns Reader collection */ public getRootReader({useGitignore = true}: { - useGitignore?: object; + useGitignore?: boolean; } = {}) { return createReader({ fsBasePath: this.getRootPath(), @@ -247,13 +280,13 @@ class Specification { private async _dirExists(dirPath: string) { const resource = await this.getRootReader().byPath(dirPath, {nodir: false}); - if (resource && resource.getStatInfo().isDirectory()) { + if (resource?.getStatInfo().isDirectory()) { return true; } return false; } - _migrateLegacyProject(config) { + _migrateLegacyProject(config: LegacySpecificationConfiguration) { // Stick to 2.6 since 3.0 adds further restrictions (i.e. for the name) and enables // functionality for extensions that shouldn't be enabled if the specVersion is not // explicitly set to 3.x @@ -264,20 +297,15 @@ class Specification { // Adding back the old default if no configuration is provided. if (config.kind === "project" && ["application", "library"].includes(config.type) && !config.resources?.configuration?.propertiesFileSourceEncoding) { - config.resources = config.resources || {}; - config.resources.configuration = config.resources.configuration || {}; + config.resources = config.resources ?? {}; + config.resources.configuration = config.resources.configuration ?? {}; config.resources.configuration.propertiesFileSourceEncoding = "ISO-8859-1"; } } } -/** - * - * @param moduleName - * @param params - */ -async function createAndInitializeSpec(moduleName, params) { - const {default: Spec} = await import(`./${moduleName}`); +async function createAndInitializeSpec(moduleName: string, params: SpecificationParameters) { + const {default: Spec} = await import(`./${moduleName}`) as {default: typeof Specification}; return new Spec().init(params); }