From a8d996fef7c8c09dad9ce4c1d434a256a674c441 Mon Sep 17 00:00:00 2001 From: Merlin Beutlberger Date: Wed, 18 Jan 2023 19:37:38 +0100 Subject: [PATCH] [INTERNAL] graph#utils: Add workspace schema validation --- lib/graph/graph.js | 68 ++++++++++++++++++++++++++++++++--------- test/lib/graph/graph.js | 12 +++----- 2 files changed, 57 insertions(+), 23 deletions(-) diff --git a/lib/graph/graph.js b/lib/graph/graph.js index 58ffa70a6..20b90b1f2 100644 --- a/lib/graph/graph.js +++ b/lib/graph/graph.js @@ -2,9 +2,12 @@ import path from "node:path"; import projectGraphBuilder from "./projectGraphBuilder.js"; import ui5Framework from "./helpers/ui5Framework.js"; import Workspace from "./Workspace.js"; +import {validateWorkspace} from "../validation/validator.js"; import {getLogger} from "@ui5/logger"; const log = getLogger("generateProjectGraph"); +const DEFAULT_WORKSPACE_CONFIG_PATH = "ui5-workspace.yaml"; + function resolveProjectPaths(cwd, project) { if (!project.path) { throw new Error(`Missing or empty attribute 'path' for project ${project.id}`); @@ -58,7 +61,7 @@ function resolveProjectPaths(cwd, project) { export async function graphFromPackageDependencies({ cwd, rootConfiguration, rootConfigPath, versionOverride, resolveFrameworkDependencies = true, - activeWorkspace="default", workspaceConfiguration, workspaceConfigPath + activeWorkspace="default", workspaceConfiguration, workspaceConfigPath = DEFAULT_WORKSPACE_CONFIG_PATH }) { log.verbose(`Creating project graph using npm provider...`); const { @@ -69,7 +72,6 @@ export async function graphFromPackageDependencies({ let workspace; if (workspaceConfiguration) { - // TODO 3.0: Schema validation if (workspaceConfiguration.metadata.name !== activeWorkspace) { log.warn( `Provided workspace configuration name "${workspaceConfiguration.metadata.name}" ` + @@ -82,11 +84,7 @@ export async function graphFromPackageDependencies({ }); } } else { - const throwIfMissing = !!workspaceConfigPath; - if (!workspaceConfigPath) { - workspaceConfigPath = "ui5-workspace.yaml"; - } - const workspaceConfigs = await utils.readWorkspaceConfigFile(cwd, workspaceConfigPath, throwIfMissing); + const workspaceConfigs = await utils.readWorkspaceConfigFile(cwd, workspaceConfigPath); const workspaceConfiguration = workspaceConfigs.find((config) => { return config.metadata.name === activeWorkspace; }); @@ -229,7 +227,7 @@ const utils = { } return dependencyTree; }, - readWorkspaceConfigFile: async function(cwd, configPath, throwIfMissing = false) { + readWorkspaceConfigFile: async function(cwd, configPath) { const { default: fs } = await import("graceful-fs"); @@ -242,26 +240,66 @@ const utils = { filePath = path.join(cwd, configPath); } + let fileContent; + try { + fileContent = await readFile(filePath, {encoding: "utf8"}); + } catch (err) { + if (err.code === "ENOENT" && configPath === DEFAULT_WORKSPACE_CONFIG_PATH ) { + log.verbose(`No workspace configuration file provided at ${filePath}`); + return []; + } + throw new Error( + `Failed to load workspace configuration from path ${filePath}: ${err.message}`); + } let configs; try { - const contents = await readFile(filePath, {encoding: "utf-8"}); - configs = jsyaml.loadAll(contents, undefined, { + configs = jsyaml.loadAll(fileContent, undefined, { filename: filePath, }); - // TODO 3.0: Add schema validation } catch (err) { if (err.name === "YAMLException") { throw new Error("Failed to parse configuration for project " + `${this.getId()} at '${filePath}'\nError: ${err.message}`); - } else if (err.code === "ENOENT" && !throwIfMissing) { - log.verbose(`No workspace configuration file provided at ${filePath}`); - return []; } else { throw new Error( - `Failed to load workspace configuration from path ${filePath}: ${err.message}`); + `Failed to parse workspace configuration at ${filePath}: ${err.message}`); } } + if (!configs || !configs.length) { + // No configs found => exit here + log.verbose(`Found empty workspace configuration file at ${filePath}`); + return configs; + } + + // Validate found configurations with schema + const validationResults = await Promise.all( + configs.map(async (config, documentIndex) => { + // Catch validation errors to ensure proper order of rejections within Promise.all + try { + await validateWorkspace({ + config, + yaml: { + path: filePath, + source: fileContent, + documentIndex + }, + schemaName: "ui5-workspace" + }); + } catch (error) { + return error; + } + }) + ); + + const validationErrors = validationResults.filter(($) => $); + + if (validationErrors.length > 0) { + // Throw any validation errors + // For now just throw the error of the first invalid document + throw validationErrors[0]; + } + return configs; } }; diff --git a/test/lib/graph/graph.js b/test/lib/graph/graph.js index 25e8ac0a8..a5d2029cd 100644 --- a/test/lib/graph/graph.js +++ b/test/lib/graph/graph.js @@ -223,8 +223,6 @@ test.serial("graphFromPackageDependencies with workspace file", async (t) => { "readWorkspaceConfigFile got called with correct first argument"); t.is(readWorkspaceConfigFileStub.getCall(0).args[1], "ui5-workspace.yaml", "readWorkspaceConfigFile got called with correct second argument"); - t.is(readWorkspaceConfigFileStub.getCall(0).args[2], false, - "readWorkspaceConfigFile got called with correct third argument"); t.is(workspaceConstructorStub.callCount, 1, "Workspace constructor got called once"); t.deepEqual(workspaceConstructorStub.getCall(0).args[0], { @@ -292,8 +290,6 @@ test.serial("graphFromPackageDependencies with inactive workspace file at custom "readWorkspaceConfigFile got called with correct first argument"); t.is(readWorkspaceConfigFileStub.getCall(0).args[1], "workspaceConfigPath", "readWorkspaceConfigFile got called with correct second argument"); - t.is(readWorkspaceConfigFileStub.getCall(0).args[2], true, - "readWorkspaceConfigFile got called with correct third argument"); t.is(workspaceConstructorStub.callCount, 0, "Workspace constructor is not called"); @@ -496,7 +492,7 @@ test.serial("utils: readWorkspaceConfigFile", async (t) => { }], "Returned correct file content"); }); -test.serial("utils: readWorkspaceConfigFile - throwIfMissing: false", async (t) => { +test.serial("utils: readWorkspaceConfigFile - Does not throw if default file is missing", async (t) => { const {graphFromPackageDependencies} = t.context.graph; const res = await graphFromPackageDependencies._utils.readWorkspaceConfigFile( path.join(fixturesPath, "library.d"), "ui5-workspace.yaml"); @@ -504,11 +500,11 @@ test.serial("utils: readWorkspaceConfigFile - throwIfMissing: false", async (t) t.deepEqual(res, [], "Returned empty array"); }); -test.serial("utils: readWorkspaceConfigFile - throwIfMissing: true", async (t) => { +test.serial("utils: readWorkspaceConfigFile - Throws if non-default file is missing", async (t) => { const {graphFromPackageDependencies} = t.context.graph; const err = await t.throwsAsync(graphFromPackageDependencies._utils.readWorkspaceConfigFile( - path.join(fixturesPath, "library.d"), "ui5-workspace.yaml", true)); - const filePath = path.join(fixturesPath, "library.d", "ui5-workspace.yaml"); + path.join(fixturesPath, "library.d"), "other-ui5-workspace.yaml", true)); + const filePath = path.join(fixturesPath, "library.d", "other-ui5-workspace.yaml"); t.is(err.message, `Failed to load workspace configuration from path ${filePath}: ` + `ENOENT: no such file or directory, open '${filePath}'`);