Skip to content

Commit

Permalink
[INTERNAL] TypeScript: Add typings to BuildContext, Specification, Pr…
Browse files Browse the repository at this point in the history
…ojectGraph
  • Loading branch information
RandomByte committed Aug 28, 2024
1 parent f8f521c commit 0aad037
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 79 deletions.
2 changes: 1 addition & 1 deletion src/build/helpers/BuildContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import OutputStyleEnum from "./ProjectBuilderOutputStyle.js";
*
*/
class BuildContext {
constructor(graph, taskRepository, { // buildConfig
constructor(graph: object, taskRepository: typeof import("@ui5/builder/internal/taskRepository"), { // buildConfig
selfContained = false,
cssVariables = false,
jsdoc = false,
Expand Down
12 changes: 7 additions & 5 deletions src/build/helpers/TaskUtil.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {ResourceInterface} from "@ui5/fs/Resource";
import {
createReaderCollection,
createReaderCollectionPrioritized,
Expand All @@ -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 {
Expand Down Expand Up @@ -47,7 +47,9 @@ class TaskUtil {
* @param parameters
* @param parameters.projectBuildContext ProjectBuildContext
*/
constructor({projectBuildContext}: object) {
STANDARD_TAGS: object;

constructor({projectBuildContext}) {
this._projectBuildContext = projectBuildContext;
/**
*/
Expand Down Expand Up @@ -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");
Expand All @@ -101,7 +103,7 @@ class TaskUtil {
* @returns Tag value for the given resource.
* <code>undefined</code> 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");
Expand All @@ -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");
Expand Down
2 changes: 1 addition & 1 deletion src/build/helpers/createBuildManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'`);
}
Expand Down
105 changes: 64 additions & 41 deletions src/graph/ProjectGraph.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
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 Specification from "../specifications/Specification.js";
import type * as T_taskRepository from "@ui5/builder/internal/taskRepository";
const log = getLogger("graph:ProjectGraph");

type TraversalCallback = (arg: {project: Project; dependencies: string[]}) => Promise<undefined>;
type VisitedNodes = Record<string, Promise<undefined> | undefined>;

/**
* A rooted, directed graph representing a UI5 project, its dependencies and available extensions.
* <br><br>
* 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<string, Project>;
_adjList: Map<string, Set<string>>;
_optAdjList: Map<string, Set<string>>;
_extensions: Map<string, Extension>;

_sealed: boolean;
_hasUnresolvedOptionalDependencies: boolean;
_taskRepository: typeof T_taskRepository | null;

/**
* @param parameters Parameters
* @param parameters.rootProjectName Root project name
Expand Down Expand Up @@ -38,7 +55,7 @@ class ProjectGraph {
*
* @returns Root project
*/
public getRoot() {
public getRoot(): Specification {
const rootProject = this._projects.get(this._rootProjectName);
if (!rootProject) {
throw new Error(`Unable to find root project with name ${this._rootProjectName} in project graph`);
Expand All @@ -51,15 +68,15 @@ 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)) {
throw new Error(
`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(
Expand Down Expand Up @@ -114,7 +131,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)) {
Expand All @@ -123,7 +140,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(
Expand Down Expand Up @@ -171,9 +188,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;
}
}

Expand All @@ -190,9 +210,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;
}
}

Expand All @@ -203,7 +226,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`);
Expand All @@ -216,7 +239,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 {
Expand Down Expand Up @@ -254,8 +277,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);
Expand Down Expand Up @@ -287,7 +310,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;
}
Expand Down Expand Up @@ -340,26 +363,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);
Expand All @@ -386,20 +409,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]) {
Expand All @@ -425,7 +450,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()) {
Expand All @@ -450,7 +475,7 @@ class ProjectGraph {
}

// Only to be used by @ui5/builder tests to inject its version of the taskRepository
setTaskRepository(taskRepository) {
setTaskRepository(taskRepository: typeof T_taskRepository) {
this._taskRepository = taskRepository;
}

Expand All @@ -459,9 +484,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;
Expand Down Expand Up @@ -530,7 +558,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
Expand All @@ -543,12 +571,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<string, string | Set<string>>, source: Map<string, string | Set<string>>) {
for (const [key, value] of source) {
if (target.has(key)) {
throw new Error(`Failed to merge map: Key '${key}' already present in target set`);
Expand Down
1 change: 0 additions & 1 deletion src/specifications/Project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import ResourceTagCollection from "@ui5/fs/internal/ResourceTagCollection";
/**
* Project
*
* @alias @ui5/project/specifications/Project
* @hideconstructor
*/
class Project extends Specification {
Expand Down
Loading

0 comments on commit 0aad037

Please sign in to comment.