diff --git a/index.js b/index.js index 8779e5963..dd95a207b 100644 --- a/index.js +++ b/index.js @@ -14,6 +14,15 @@ module.exports = { Openui5Resolver: require("./lib/ui5Framework/Openui5Resolver"), Sapui5Resolver: require("./lib/ui5Framework/Sapui5Resolver") }, + /** + * @public + * @see module:@ui5/project.validation + * @namespace + */ + validation: { + validator: require("./lib/validation/validator"), + ValidationError: require("./lib/validation/ValidationError") + }, /** * @private * @see module:@ui5/project.translators diff --git a/lib/projectPreprocessor.js b/lib/projectPreprocessor.js index 4f404e3ba..f6e2402d6 100644 --- a/lib/projectPreprocessor.js +++ b/lib/projectPreprocessor.js @@ -3,8 +3,9 @@ const fs = require("graceful-fs"); const path = require("path"); const {promisify} = require("util"); const readFile = promisify(fs.readFile); -const parseYaml = require("js-yaml").safeLoadAll; +const jsyaml = require("js-yaml"); const typeRepository = require("@ui5/builder").types.typeRepository; +const {validate} = require("./validation/validator"); class ProjectPreprocessor { constructor({tree}) { @@ -80,7 +81,7 @@ class ProjectPreprocessor { return this.applyExtension(extProject); })); } - this.applyShims(project); + await this.applyShims(project); if (this.isConfigValid(project)) { // Do not apply transparent projects. // Their only purpose might be to have their dependencies processed @@ -194,26 +195,19 @@ class ProjectPreprocessor { async loadProjectConfiguration(project) { if (project.specVersion) { // Project might already be configured // Currently, specVersion is the indicator for configured projects - this.normalizeConfig(project); - return {}; - } - let configs; + if (project._transparentProject) { + // Assume that project is already processed + return {}; + } - // A projects configPath property takes precedence over the default "/ui5.yaml" path - const configPath = project.configPath || path.join(project.path, "/ui5.yaml"); - try { - configs = await this.readConfigFile(configPath); - } catch (err) { - const errorText = "Failed to read configuration for project " + - `${project.id} at "${configPath}". Error: ${err.message}`; + await this.validateAndNormalizeExistingProject(project); - if (err.code !== "ENOENT") { // Something else than "File or directory does not exist" - throw new Error(errorText); - } - log.verbose(errorText); + return {}; } + const configs = await this.readConfigFile(project); + if (!configs || !configs.length) { return {}; } @@ -384,11 +378,77 @@ class ProjectPreprocessor { } } - async readConfigFile(configPath) { - const configFile = await readFile(configPath); - return parseYaml(configFile, { - filename: path - }); + async readConfigFile(project) { + // A projects configPath property takes precedence over the default "/ui5.yaml" path + const configPath = project.configPath || path.join(project.path, "ui5.yaml"); + let configFile; + try { + configFile = await readFile(configPath, {encoding: "utf8"}); + } catch (err) { + const errorText = "Failed to read configuration for project " + + `${project.id} at "${configPath}". Error: ${err.message}`; + + // Something else than "File or directory does not exist" or root project + if (err.code !== "ENOENT" || project._level === 0) { + throw new Error(errorText); + } else { + log.verbose(errorText); + return null; + } + } + + let configs; + + try { + // Using loadAll with DEFAULT_SAFE_SCHEMA instead of safeLoadAll to pass "filename". + // safeLoadAll doesn't handle its parameters properly. + // See https://github.com/nodeca/js-yaml/issues/456 and https://github.com/nodeca/js-yaml/pull/381 + configs = jsyaml.loadAll(configFile, undefined, { + filename: configPath, + schema: jsyaml.DEFAULT_SAFE_SCHEMA + }); + } catch (err) { + if (err.name === "YAMLException") { + throw new Error("Failed to parse configuration for project " + + `${project.id} at "${configPath}"\nError: ${err.message}`); + } else { + throw err; + } + } + + if (!configs || !configs.length) { + return configs; + } + + const validationResults = await Promise.all( + configs.map(async (config, documentIndex) => { + // Catch validation errors to ensure proper order of rejections within Promise.all + try { + await validate({ + config, + project: { + id: project.id + }, + yaml: { + path: configPath, + source: configFile, + documentIndex + } + }); + } catch (error) { + return error; + } + }) + ); + + const validationErrors = validationResults.filter(($) => $); + + if (validationErrors.length > 0) { + // For now just throw the error of the first invalid document + throw validationErrors[0]; + } + + return configs; } handleShim(extension) { @@ -451,7 +511,7 @@ class ProjectPreprocessor { } } - applyShims(project) { + async applyShims(project) { const configShim = this.configShims[project.id]; // Apply configuration shims if (configShim) { @@ -482,6 +542,8 @@ class ProjectPreprocessor { Object.assign(project, configShim); delete project.shimDependenciesResolved; // Remove shim processing metadata from project + + await this.validateAndNormalizeExistingProject(project); } // Apply collections @@ -539,6 +601,31 @@ class ProjectPreprocessor { const middlewarePath = path.join(extension.path, extension.middleware.path); middlewareRepository.addMiddleware(extension.metadata.name, middlewarePath); } + + async validateAndNormalizeExistingProject(project) { + // Validate project config, but exclude additional properties + const excludedProperties = [ + "id", + "version", + "path", + "dependencies", + "_level" + ]; + const config = {}; + for (const key in project) { + if (project.hasOwnProperty(key) && !excludedProperties.includes(key)) { + config[key] = project[key]; + } + } + await validate({ + config, + project: { + id: project.id + } + }); + + this.normalizeConfig(project); + } } /** diff --git a/lib/validation/ValidationError.js b/lib/validation/ValidationError.js new file mode 100644 index 000000000..bc21e9630 --- /dev/null +++ b/lib/validation/ValidationError.js @@ -0,0 +1,274 @@ +const chalk = require("chalk"); +const escapeStringRegExp = require("escape-string-regexp"); +const matchAll = require("string.prototype.matchall"); + +/** + * Error class for validation of project configuration. + * + * @public + * @hideconstructor + * @extends Error + * @memberof module:@ui5/project.validation + */ +class ValidationError extends Error { + constructor({errors, project, yaml}) { + super(); + + /** + * ValidationError + * + * @const + * @default + * @type {string} + * @readonly + * @public + */ + this.name = "ValidationError"; + + this.project = project; + this.yaml = yaml; + + this.errors = ValidationError.filterErrors(errors); + + /** + * Formatted error message + * + * @type {string} + * @readonly + * @public + */ + this.message = this.formatErrors(); + + Error.captureStackTrace(this, this.constructor); + } + + formatErrors() { + let separator = "\n\n"; + if (process.stdout.isTTY) { + // Add a horizontal separator line between errors in case a terminal is used + separator += chalk.grey.dim("\u2500".repeat(process.stdout.columns || 80)); + } + separator += "\n\n"; + let message = chalk.red(`Invalid ui5.yaml configuration for project ${this.project.id}`) + "\n\n"; + message += this.errors.map((error) => { + return this.formatError(error); + }).join(separator); + return message; + } + + formatError(error) { + let errorMessage = ValidationError.formatMessage(error); + if (this.yaml && this.yaml.path && this.yaml.source) { + const yamlExtract = ValidationError.getYamlExtract({error, yaml: this.yaml}); + const errorLines = errorMessage.split("\n"); + errorLines.splice(1, 0, "\n" + yamlExtract); + errorMessage = errorLines.join("\n"); + } + return errorMessage; + } + + static formatMessage(error) { + if (error.keyword === "errorMessage") { + return error.message; + } + + let message = "Configuration "; + if (error.dataPath) { + message += chalk.underline(chalk.red(error.dataPath.substr(1))) + " "; + } + + switch (error.keyword) { + case "additionalProperties": + message += `property ${error.params.additionalProperty} is not expected to be here`; + break; + case "type": + message += `should be of type '${error.params.type}'`; + break; + case "enum": + message += error.message + "\n"; + message += "Allowed values: " + error.params.allowedValues.join(", "); + break; + default: + message += error.message; + } + + return message; + } + + static _findDuplicateError(error, errorIndex, errors) { + const foundIndex = errors.findIndex(($) => { + if ($.dataPath !== error.dataPath) { + return false; + } else if ($.keyword !== error.keyword) { + return false; + } else if (JSON.stringify($.params) !== JSON.stringify(error.params)) { + return false; + } else { + return true; + } + }); + return foundIndex !== errorIndex; + } + + static filterErrors(allErrors) { + return allErrors.filter((error, i, errors) => { + if (error.keyword === "if" || error.keyword === "oneOf") { + return false; + } + + return !ValidationError._findDuplicateError(error, i, errors); + }); + } + + static analyzeYamlError({error, yaml}) { + if (error.dataPath === "" && error.keyword === "required") { + // There is no line/column for a missing required property on root level + return {line: -1, column: -1}; + } + + // Skip leading / + const objectPath = error.dataPath.substr(1).split("/"); + + if (error.keyword === "additionalProperties") { + objectPath.push(error.params.additionalProperty); + } + + let currentSubstring; + let currentIndex; + if (yaml.documentIndex) { + const matchDocumentSeparator = /^---/gm; + const documents = matchAll(yaml.source, matchDocumentSeparator); + let currentDocumentIndex = 0; + for (const document of documents) { + // If the first separator is not at the beginning of the file + // we are already at document index 1 + // Using String#trim() to remove any whitespace characters + if (currentDocumentIndex === 0 && yaml.source.substring(0, document.index).trim().length > 0) { + currentDocumentIndex = 1; + } + + if (currentDocumentIndex === yaml.documentIndex) { + currentIndex = document.index; + currentSubstring = yaml.source.substring(currentIndex); + break; + } + + currentDocumentIndex++; + } + // Document could not be found + if (!currentSubstring) { + return {line: -1, column: -1}; + } + } else { + // In case of index 0 or no index, use whole source + currentIndex = 0; + currentSubstring = yaml.source; + } + + + const matchArrayElement = /(^|\r?\n)([ ]*-[^\r\n]*)/g; + const matchArrayElementIndentation = /([ ]*)-/; + + for (let i = 0; i < objectPath.length; i++) { + const property = objectPath[i]; + let newIndex; + + if (isNaN(property)) { + // Try to find a property + + // Creating a regular expression that matches the property name a line + // except for comments, indicated by a hash sign "#". + const propertyRegExp = new RegExp(`^[^#]*?${escapeStringRegExp(property)}`, "m"); + + const propertyMatch = propertyRegExp.exec(currentSubstring); + if (!propertyMatch) { + return {line: -1, column: -1}; + } + newIndex = propertyMatch.index + propertyMatch[0].length; + } else { + // Try to find the right index within an array definition. + // This currently only works for arrays defined with "-" in multiple lines. + // Arrays using square brackets are not supported. + + const arrayIndex = parseInt(property); + const arrayIndicators = matchAll(currentSubstring, matchArrayElement); + let a = 0; + let firstIndentation = -1; + for (const match of arrayIndicators) { + const indentationMatch = match[2].match(matchArrayElementIndentation); + if (!indentationMatch) { + return {line: -1, column: -1}; + } + const currentIndentation = indentationMatch[1].length; + if (firstIndentation === -1) { + firstIndentation = currentIndentation; + } else if (currentIndentation !== firstIndentation) { + continue; + } + if (a === arrayIndex) { + // match[1] might be a line-break + newIndex = match.index + match[1].length + currentIndentation; + break; + } + a++; + } + if (!newIndex) { + // Could not find array element + return {line: -1, column: -1}; + } + } + currentIndex += newIndex; + currentSubstring = yaml.source.substring(currentIndex); + } + + const linesUntilMatch = yaml.source.substring(0, currentIndex).split(/\r?\n/); + const line = linesUntilMatch.length; + let column = linesUntilMatch[line - 1].length + 1; + const lastPathSegment = objectPath[objectPath.length - 1]; + if (isNaN(lastPathSegment)) { + column -= lastPathSegment.length; + } + + return { + line, + column + }; + } + + static getSourceExtract(yamlSource, line, column) { + let source = ""; + const lines = yamlSource.split(/\r?\n/); + + // Using line numbers instead of array indices + const startLine = Math.max(line - 2, 1); + const endLine = Math.min(line, lines.length); + const padLength = String(endLine).length; + + for (let currentLine = startLine; currentLine <= endLine; currentLine++) { + const currentLineContent = lines[currentLine - 1]; + let string = chalk.gray( + String(currentLine).padStart(padLength, " ") + ":" + ) + " " + currentLineContent + "\n"; + if (currentLine === line) { + string = chalk.bgRed(string); + } + source += string; + } + + source += " ".repeat(column + padLength + 1) + chalk.red("^"); + + return source; + } + + static getYamlExtract({error, yaml}) { + const {line, column} = ValidationError.analyzeYamlError({error, yaml}); + if (line !== -1 && column !== -1) { + return chalk.grey(yaml.path + ":" + line) + + "\n\n" + ValidationError.getSourceExtract(yaml.source, line, column); + } else { + return chalk.grey(yaml.path) + "\n"; + } + } +} + +module.exports = ValidationError; diff --git a/lib/validation/schema/specVersion/2.0.json b/lib/validation/schema/specVersion/2.0.json new file mode 100644 index 000000000..27bd1f9e8 --- /dev/null +++ b/lib/validation/schema/specVersion/2.0.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/specVersion/2.0.json", + + "type": "object", + "required": ["specVersion", "metadata"], + "properties": { + "specVersion": { "enum": ["2.0"] }, + "kind": { + "enum": ["project", "extension", null], + "$comment": "Using null to allow not defining 'kind' which defaults to project" + }, + "metadata": { + "$ref": "../ui5.json#/definitions/metadata" + } + }, + "if": { + "properties": { + "kind": { + "enum": ["project", null], + "$comment": "Using null to allow not defining 'kind' which defaults to project" + } + } + }, + "then": { + "$ref": "2.0/kind/project.json" + }, + "else": { + "if": { + "properties": { + "kind": { + "enum": ["extension"] + } + } + }, + "then": { + "$ref": "2.0/kind/extension.json" + } + } +} diff --git a/lib/validation/schema/specVersion/2.0/kind/extension.json b/lib/validation/schema/specVersion/2.0/kind/extension.json new file mode 100644 index 000000000..d49c9613d --- /dev/null +++ b/lib/validation/schema/specVersion/2.0/kind/extension.json @@ -0,0 +1,60 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/specVersion/2.0/kind/extension.json", + + "type": "object", + "required": ["specVersion", "kind", "type", "metadata"], + "properties": { + "specVersion": { "enum": ["2.0"] }, + "kind": { + "enum": ["extension"] + }, + "type": { + "enum": [ + "task", + "server-middleware", + "project-shim" + ] + }, + "metadata": { + "$ref": "../../../ui5.json#/definitions/metadata" + } + }, + "if": { + "properties": { + "type": {"const": null} + }, + "$comment": "Using 'if' with null and empty 'then' to ensure no other schemas are applied when the property is not set. Otherwise the first 'if' condition might still be met, causing unexpected errors." + }, + "then": {}, + "else": { + "if": { + "properties": { + "type": {"const": "task"} + } + }, + "then": { + "$ref": "extension/task.json" + }, + "else": { + "if": { + "properties": { + "type": {"const": "server-middleware"} + } + }, + "then": { + "$ref": "extension/server-middleware.json" + }, + "else": { + "if": { + "properties": { + "type": {"const": "project-shim"} + } + }, + "then": { + "$ref": "extension/project-shim.json" + } + } + } + } +} diff --git a/lib/validation/schema/specVersion/2.0/kind/extension/project-shim.json b/lib/validation/schema/specVersion/2.0/kind/extension/project-shim.json new file mode 100644 index 000000000..52e986561 --- /dev/null +++ b/lib/validation/schema/specVersion/2.0/kind/extension/project-shim.json @@ -0,0 +1,68 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/specVersion/2.0/kind/extension/project-shim.json", + + "type": "object", + "additionalProperties": false, + "required": ["specVersion", "kind", "type", "metadata", "shims"], + "properties": { + "specVersion": { "enum": ["2.0"] }, + "kind": { + "enum": ["extension"] + }, + "type": { + "enum": ["project-shim"] + }, + "metadata": { + "$ref": "../../../../ui5.json#/definitions/metadata" + }, + "shims": { + "type": "object", + "additionalProperties": false, + "properties": { + "configurations": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + ".+": { + "type": "object" + } + } + }, + "dependencies": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + ".+": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "collections": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + ".+": { + "type": "object", + "additionalProperties": false, + "properties": { + "modules": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + ".+": { + "type": "string" + } + } + } + } + } + } + } + } + } + } +} diff --git a/lib/validation/schema/specVersion/2.0/kind/extension/server-middleware.json b/lib/validation/schema/specVersion/2.0/kind/extension/server-middleware.json new file mode 100644 index 000000000..82e686927 --- /dev/null +++ b/lib/validation/schema/specVersion/2.0/kind/extension/server-middleware.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/specVersion/2.0/kind/extension/server-middleware.json", + + "type": "object", + "additionalProperties": false, + "required": ["specVersion", "kind", "type", "metadata", "middleware"], + "properties": { + "specVersion": { "enum": ["2.0"] }, + "kind": { + "enum": ["extension"] + }, + "type": { + "enum": ["server-middleware"] + }, + "metadata": { + "$ref": "../../../../ui5.json#/definitions/metadata" + }, + "middleware": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string" + } + } + } + } +} diff --git a/lib/validation/schema/specVersion/2.0/kind/extension/task.json b/lib/validation/schema/specVersion/2.0/kind/extension/task.json new file mode 100644 index 000000000..0dd6a7a02 --- /dev/null +++ b/lib/validation/schema/specVersion/2.0/kind/extension/task.json @@ -0,0 +1,29 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/specVersion/2.0/kind/extension/task.json", + + "type": "object", + "additionalProperties": false, + "required": ["specVersion", "kind", "type", "metadata", "task"], + "properties": { + "specVersion": { "enum": ["2.0"] }, + "kind": { + "enum": ["extension"] + }, + "type": { + "enum": ["task"] + }, + "metadata": { + "$ref": "../../../../ui5.json#/definitions/metadata" + }, + "task": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string" + } + } + } + } +} diff --git a/lib/validation/schema/specVersion/2.0/kind/project.json b/lib/validation/schema/specVersion/2.0/kind/project.json new file mode 100644 index 000000000..17a82151e --- /dev/null +++ b/lib/validation/schema/specVersion/2.0/kind/project.json @@ -0,0 +1,330 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/specVersion/2.0/kind/project.json", + + "type": "object", + "required": ["specVersion", "type", "metadata"], + "properties": { + "specVersion": { "enum": ["2.0"] }, + "kind": { + "enum": ["project", null], + "$comment": "Using null to allow not defining 'kind' which defaults to project" + }, + "type": { + "enum": [ + "application", + "library", + "theme-library", + "module" + ] + }, + "metadata": { + "$ref": "../../../ui5.json#/definitions/metadata" + } + }, + "if": { + "properties": { + "type": {"const": null} + }, + "$comment": "Using 'if' with null and empty 'then' to ensure no other schemas are applied when the property is not set. Otherwise the first 'if' condition might still be met, causing unexpected errors." + }, + "then": {}, + "else": { + "if": { + "properties": { + "type": {"const": "application"} + } + }, + "then": { + "$ref": "project/application.json" + }, + "else": { + "if": { + "properties": { + "type": {"const": "library"} + } + }, + "then": { + "$ref": "project/library.json" + }, + "else": { + "if": { + "properties": { + "type": {"const": "theme-library"} + } + }, + "then": { + "$ref": "project/theme-library.json" + }, + "else": { + "if": { + "properties": { + "type": {"const": "module"} + } + }, + "then": { + "$ref": "project/module.json" + } + } + } + } + }, + + "definitions": { + "resources-configuration-propertiesFileSourceEncoding": { + "enum": ["UTF-8", "ISO-8859-1"], + "default": "ISO-8859-1", + "title": "Encoding of *.properties files", + "description": "By default, the UI5 Tooling expects *.properties files to be ISO-8859-1 encoded." + }, + "builder-resources": { + "type": "object", + "additionalProperties": false, + "properties": { + "excludes": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "builder-bundles": { + "type": "array", + "additionalProperties": false, + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "bundleDefinition": { + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "defaultFileTypes": { + "type": "array", + "items": { + "type": "string" + } + }, + "sections": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["mode", "filters"], + "properties": { + "mode": { + "enum": ["raw", "preload", "require", "provided"] + }, + "filters": { + "type": "array", + "items": { + "type": "string" + } + }, + "resolve": { + "type": "boolean", + "default": false + }, + "resolveConditional": { + "type": "boolean", + "default": false + }, + "renderer": { + "type": "boolean", + "default": false + }, + "sort": { + "type": "boolean", + "default": true + } + } + } + } + } + }, + "bundleOptions": { + "type": "object", + "additionalProperties": false, + "properties": { + "optimize": { + "type": "boolean", + "default": false + }, + "decorateBootstrapModule": { + "type": "boolean", + "default": true + }, + "addTryCatchRestartWrapper": { + "type": "boolean", + "default": false + }, + "usePredefineCalls": { + "type": "boolean", + "default": false + }, + "numberOfParts": { + "type": "number", + "default": 1 + } + } + } + } + } + }, + "builder-componentPreload": { + "type": "object", + "additionalProperties": false, + "properties": { + "paths": { + "type": "array", + "items": { + "type": "string" + } + }, + "namespaces": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "server": { + "type": "object", + "additionalProperties": false, + "properties": { + "settings": { + "type": "object", + "additionalProperties": false, + "properties": { + "httpPort": { + "type": "number" + }, + "httpsPort": { + "type": "number" + } + } + }, + "customMiddleware": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["name", "beforeMiddleware"], + "properties": { + "name": { + "type": "string" + }, + "mountPath": { + "type": "string" + }, + "beforeMiddleware": { + "type": "string" + }, + "configuration": {} + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["name", "afterMiddleware"], + "properties": { + "name": { + "type": "string" + }, + "mountPath": { + "type": "string" + }, + "afterMiddleware": { + "type": "string" + }, + "configuration": {} + } + } + ] + } + } + } + }, + "framework": { + "type": "object", + "additionalProperties": false, + "required": ["name", "version"], + "properties": { + "name": { + "enum": ["OpenUI5", "SAPUI5"] + }, + "version": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$", + "title": "Version", + "description": "Framework version to use in this project", + "errorMessage": "Not a valid version according to the Semantic Versioning specification (https://semver.org/)" + }, + "libraries": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": { + "type": "string" + }, + "optional": { + "type": "boolean", + "default": false + }, + "development": { + "type": "boolean", + "default": false + } + } + } + } + } + }, + "customTasks": { + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["name", "beforeTask"], + "properties": { + "name": { + "type": "string" + }, + "beforeTask": { + "type": "string" + }, + "configuration": {} + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["name", "afterTask"], + "properties": { + "name": { + "type": "string" + }, + "afterTask": { + "type": "string" + }, + "configuration": {} + } + } + ] + } + } + } +} diff --git a/lib/validation/schema/specVersion/2.0/kind/project/application.json b/lib/validation/schema/specVersion/2.0/kind/project/application.json new file mode 100644 index 000000000..8ad7f7120 --- /dev/null +++ b/lib/validation/schema/specVersion/2.0/kind/project/application.json @@ -0,0 +1,88 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/specVersion/2.0/kind/project/application.json", + + "type": "object", + "additionalProperties": false, + "required": ["specVersion", "type", "metadata"], + "properties": { + "specVersion": { "enum": ["2.0"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["application"] + }, + "metadata": { + "$ref": "../../../../ui5.json#/definitions/metadata" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder" + }, + "server": { + "$ref": "../project.json#/definitions/server" + } + }, + + "definitions": { + + "resources": { + "type": "object", + "additionalProperties": false, + "properties": { + "configuration": { + "type": "object", + "additionalProperties": false, + "properties": { + "propertiesFileSourceEncoding": { + "$ref": "../project.json#/definitions/resources-configuration-propertiesFileSourceEncoding" + }, + "paths": { + "type": "object", + "additionalProperties": false, + "properties": { + "webapp": { + "type": "string" + } + } + } + } + } + } + }, + + "builder": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "cachebuster": { + "type": "object", + "additionalProperties": false, + "properties": { + "signatureType": { + "enum": ["time", "hash"] + } + } + }, + "bundles": { + "$ref": "../project.json#/definitions/builder-bundles" + }, + "componentPreload": { + "$ref": "../project.json#/definitions/builder-componentPreload" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + } + } + } + } +} diff --git a/lib/validation/schema/specVersion/2.0/kind/project/library.json b/lib/validation/schema/specVersion/2.0/kind/project/library.json new file mode 100644 index 000000000..8d1cf93d4 --- /dev/null +++ b/lib/validation/schema/specVersion/2.0/kind/project/library.json @@ -0,0 +1,92 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/specVersion/2.0/kind/project/library.json", + + "type": "object", + "additionalProperties": false, + "required": ["specVersion", "type", "metadata"], + "properties": { + "specVersion": { "enum": ["2.0"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["library"] + }, + "metadata": { + "$ref": "../../../../ui5.json#/definitions/metadata" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder" + }, + "server": { + "$ref": "../project.json#/definitions/server" + } + }, + + "definitions": { + "resources": { + "type": "object", + "additionalProperties": false, + "properties": { + "configuration": { + "type": "object", + "additionalProperties": false, + "properties": { + "propertiesFileSourceEncoding": { + "$ref": "../project.json#/definitions/resources-configuration-propertiesFileSourceEncoding" + }, + "paths": { + "type": "object", + "additionalProperties": false, + "properties": { + "src": { + "type": "string" + }, + "test": { + "type": "string" + } + } + } + } + } + } + }, + "builder": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "jsdoc": { + "type": "object", + "additionalProperties": false, + "properties": { + "excludes": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "bundles": { + "$ref": "../project.json#/definitions/builder-bundles" + }, + "componentPreload": { + "$ref": "../project.json#/definitions/builder-componentPreload" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + } + } + } + } +} diff --git a/lib/validation/schema/specVersion/2.0/kind/project/module.json b/lib/validation/schema/specVersion/2.0/kind/project/module.json new file mode 100644 index 000000000..1ea690cc3 --- /dev/null +++ b/lib/validation/schema/specVersion/2.0/kind/project/module.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/specVersion/2.0/kind/project/module.json", + + "type": "object", + "additionalProperties": false, + "required": ["specVersion", "type", "metadata"], + "properties": { + "specVersion": { "enum": ["2.0"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["module"] + }, + "metadata": { + "$ref": "../../../../ui5.json#/definitions/metadata" + }, + "resources": { + "type": "object", + "additionalProperties": false, + "properties": { + "configuration": { + "type": "object", + "additionalProperties": false, + "properties": { + "paths": { + "type": "object", + "additionalProperties": false, + "patternProperties": { + ".+": { + "type": "string" + } + } + } + } + } + } + } + } +} diff --git a/lib/validation/schema/specVersion/2.0/kind/project/theme-library.json b/lib/validation/schema/specVersion/2.0/kind/project/theme-library.json new file mode 100644 index 000000000..46dccd07a --- /dev/null +++ b/lib/validation/schema/specVersion/2.0/kind/project/theme-library.json @@ -0,0 +1,47 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/specVersion/2.0/kind/project/theme-library.json", + + "type": "object", + "additionalProperties": false, + "required": ["specVersion", "type", "metadata"], + "properties": { + "specVersion": { "enum": ["2.0"] }, + "kind": { + "enum": ["project", null] + }, + "type": { + "enum": ["theme-library"] + }, + "metadata": { + "$ref": "../../../../ui5.json#/definitions/metadata" + }, + "framework": { + "$ref": "../project.json#/definitions/framework" + }, + "resources": { + "$ref": "library.json#/definitions/resources" + }, + "builder": { + "$ref": "#/definitions/builder" + }, + "server": { + "$ref": "../project.json#/definitions/server" + } + }, + + "definitions": { + "builder": { + "type": "object", + "additionalProperties": false, + "properties": { + "resources": { + "$ref": "../project.json#/definitions/builder-resources" + }, + "customTasks": { + "$ref": "../project.json#/definitions/customTasks" + } + } + } + } +} diff --git a/lib/validation/schema/ui5.json b/lib/validation/schema/ui5.json new file mode 100644 index 000000000..a9a5ae54f --- /dev/null +++ b/lib/validation/schema/ui5.json @@ -0,0 +1,55 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "http://ui5.sap/schema/ui5.json", + "title": "ui5.yaml", + "description": "Schema for UI5 Tooling Configuration File (ui5.yaml)", + "$comment": "See https://sap.github.io/ui5-tooling/", + + "type": "object", + "required": ["specVersion"], + "properties": { + "specVersion": { + "enum": [ + "2.0", + "1.1", "1.0", "0.1" + ], + "errorMessage": "Unsupported \"specVersion\"\nYour UI5 CLI installation might be outdated.\nSupported specification versions: \"2.0\", \"1.1\", \"1.0\", \"0.1\"\nFor details see: https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions" + } + }, + + "if": { + "properties": { + "specVersion": { "const": "2.0" } + } + }, + "then": { + "$ref": "specVersion/2.0.json" + }, + "else": { + "if": { + "properties": { + "specVersion": { "enum": ["1.1", "1.0", "0.1"] } + } + }, + "then": { + "additionalProperties": true + } + }, + + "definitions": { + + "metadata": { + "type": "object", + "required": ["name"], + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "copyright": { + "type": "string" + } + } + } + } +} diff --git a/lib/validation/validator.js b/lib/validation/validator.js new file mode 100644 index 000000000..07682a94c --- /dev/null +++ b/lib/validation/validator.js @@ -0,0 +1,83 @@ +const Ajv = require("ajv"); +const ajvErrors = require("ajv-errors"); +const path = require("path"); +const {promisify} = require("util"); +const readFile = promisify(require("fs").readFile); + +const ValidationError = require("./ValidationError"); + +async function loadSchema(schemaPath) { + const filePath = schemaPath.replace("http://ui5.sap/schema/", ""); + const schemaFile = await readFile(path.join(__dirname, "schema", filePath), {encoding: "utf8"}); + return JSON.parse(schemaFile); +} + +/** + * @private + * @memberof module:@ui5/project.validation + */ +class Validator { + constructor() { + this.ajv = new Ajv({ + allErrors: true, + jsonPointers: true, + loadSchema + }); + ajvErrors(this.ajv); + } + + _compileSchema() { + if (!this._compiling) { + this._compiling = Promise.resolve().then(async () => { + const schema = await loadSchema("ui5.json"); + const validate = await this.ajv.compileAsync(schema); + return validate; + }); + } + return this._compiling; + } + + async validate({config, project, yaml}) { + const fnValidate = await this._compileSchema(); + const valid = fnValidate(config); + if (!valid) { + throw new ValidationError({ + errors: fnValidate.errors, + schema: fnValidate.schema, + project, + yaml + }); + } + } +} + +const validator = new Validator(); + +/** + * @public + * @namespace + * @alias module:@ui5/project.validation.validator + */ +module.exports = { + /** + * Validates the given configuration. + * + * @param {Object} options + * @param {Object} options.config UI5 Configuration to validate + * @param {Object} options.project Project information + * @param {string} options.project.id ID of the project + * @param {Object} [options.yaml] YAML information + * @param {string} options.yaml.path Path of the YAML file + * @param {string} options.yaml.source Content of the YAML file + * @param {number} [options.yaml.documentIndex=0] Document index in case the YAML file contains multiple documents + * @throws {module:@ui5/project.validation.ValidationError} + * Rejects with a {@link module:@ui5/project.validation.ValidationError ValidationError} + * when the validation fails. + * @returns {Promise} Returns a Promise that resolves when the validation succeeds + * @public + */ + validate: async (options) => { + await validator.validate(options); + }, + _Validator: Validator // For testing only +}; diff --git a/package-lock.json b/package-lock.json index a7fdb2f5e..6455fbe58 100644 --- a/package-lock.json +++ b/package-lock.json @@ -286,14 +286,51 @@ } }, "@babel/highlight": { - "version": "7.9.0", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.9.0.tgz", - "integrity": "sha512-lJZPilxX7Op3Nv/2cvFdnlepPXDxi29wxteT57Q965oc5R9v86ztx0jfxVrTcBk8C2kcPkkDa2Z4T3ZsPPVWsQ==", + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz", + "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.9.0", "chalk": "^2.0.0", + "esutils": "^2.0.2", "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } } }, "@babel/parser": { @@ -643,8 +680,7 @@ "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", - "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", - "dev": true + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==" }, "@types/events": { "version": "3.0.0", @@ -753,6 +789,12 @@ "yesno": "^0.3.1" } }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -842,6 +884,11 @@ "uri-js": "^4.2.2" } }, + "ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==" + }, "ansi-align": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.0.tgz", @@ -909,7 +956,6 @@ "version": "4.2.1", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, "requires": { "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" @@ -919,7 +965,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -927,8 +972,7 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" } } }, @@ -1147,6 +1191,43 @@ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -1333,18 +1414,44 @@ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, "camelcase": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", "dev": true }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, "is-fullwidth-code-point": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", @@ -1371,6 +1478,15 @@ "ansi-regex": "^4.1.0" } }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, "type-fest": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", @@ -1549,40 +1665,12 @@ } }, "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" } }, "chardet": { @@ -2053,6 +2141,16 @@ } } }, + "config-chain": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", + "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", + "dev": true, + "requires": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, "configstore": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/configstore/-/configstore-4.0.0.tgz", @@ -2492,7 +2590,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -2734,6 +2831,42 @@ "safer-buffer": "^2.1.0" } }, + "editorconfig": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.3.tgz", + "integrity": "sha512-M9wIMFx96vq0R4F+gRpY3o2exzb8hEj/n9S8unZtHSvYjibBp/iMufSzvmOcV/laG0ZtuTVGtiJggPOSW2r93g==", + "dev": true, + "requires": { + "commander": "^2.19.0", + "lru-cache": "^4.1.5", + "semver": "^5.6.0", + "sigmund": "^1.0.1" + }, + "dependencies": { + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", + "dev": true + } + } + }, "ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2812,7 +2945,6 @@ "version": "1.17.4", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.4.tgz", "integrity": "sha512-Ae3um/gb8F0mui/jPL+QiqmglkUsaQf7FwBEHYIFkztkneosu9imhqHpBzQ3h1vit8t5iQ74t6PEVvphBZiuiQ==", - "dev": true, "requires": { "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", @@ -2831,7 +2963,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, "requires": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -3008,6 +3139,26 @@ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "dev": true }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, "cross-spawn": { "version": "6.0.5", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", @@ -3030,6 +3181,12 @@ "ms": "^2.1.1" } }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, "ignore": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", @@ -3072,6 +3229,15 @@ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -3611,8 +3777,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "functional-red-black-tree": { "version": "1.0.1", @@ -3780,7 +3945,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -3797,14 +3961,12 @@ "has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", - "dev": true + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" }, "has-symbols": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "dev": true + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" }, "has-unicode": { "version": "2.0.1", @@ -4124,6 +4286,26 @@ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", "dev": true }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, "cli-cursor": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", @@ -4216,9 +4398,28 @@ "dev": true } } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } } } }, + "internal-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.2.tgz", + "integrity": "sha512-2cQNfwhAfJIkU4KZPkDI+Gj5yNNnbqi40W9Gge6dfnk4TocEVm00B3bdiL+JINrbGJil2TeHvM4rETGzk/f/0g==", + "requires": { + "es-abstract": "^1.17.0-next.1", + "has": "^1.0.3", + "side-channel": "^1.0.2" + } + }, "ip": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", @@ -4258,8 +4459,7 @@ "is-callable": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.5.tgz", - "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==", - "dev": true + "integrity": "sha512-ESKv5sMCJB2jnHTWZ3O5itG+O128Hsus4K4Qh1h2/cgn2vbgnLSVqfV46AeJA9D5EeeLa9w81KUXMtn34zhX+Q==" }, "is-ci": { "version": "2.0.0", @@ -4273,8 +4473,7 @@ "is-date-object": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", - "dev": true + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" }, "is-error": { "version": "2.2.2", @@ -4409,7 +4608,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", - "dev": true, "requires": { "has": "^1.0.3" } @@ -4424,7 +4622,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "dev": true, "requires": { "has-symbols": "^1.0.1" } @@ -4613,6 +4810,36 @@ "istanbul-lib-report": "^3.0.0" } }, + "js-beautify": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.10.3.tgz", + "integrity": "sha512-wfk/IAWobz1TfApSdivH5PJ0miIHgDoYb1ugSqHcODPmaYu46rYe5FVuIEkhjg8IQiv6rDNPyhsqbsohI/C2vQ==", + "dev": true, + "requires": { + "config-chain": "^1.1.12", + "editorconfig": "^0.15.3", + "glob": "^7.1.3", + "mkdirp": "~0.5.1", + "nopt": "~4.0.1" + }, + "dependencies": { + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + } + } + }, "js-string-escape": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/js-string-escape/-/js-string-escape-1.0.1.tgz", @@ -4976,6 +5203,43 @@ "dev": true, "requires": { "chalk": "^2.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } } }, "loud-rejection": { @@ -5467,6 +5731,16 @@ "process-on-spawn": "^1.0.0" } }, + "nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dev": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -5710,8 +5984,7 @@ "object-inspect": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", - "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==", - "dev": true + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==" }, "object-is": { "version": "1.0.2", @@ -5722,14 +5995,12 @@ "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, "object.assign": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, "requires": { "define-properties": "^1.1.2", "function-bind": "^1.1.1", @@ -5837,6 +6108,26 @@ "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "dev": true }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, "cli-cursor": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", @@ -5846,6 +6137,12 @@ "restore-cursor": "^2.0.0" } }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, "mimic-fn": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", @@ -5879,15 +6176,40 @@ "requires": { "ansi-regex": "^4.1.0" } + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } } } }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true + }, "os-tmpdir": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", "dev": true }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, "p-cancelable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", @@ -6268,6 +6590,12 @@ "retry": "^0.10.0" } }, + "proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", + "dev": true + }, "proxy-addr": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", @@ -6359,6 +6687,14 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } } } }, @@ -6471,7 +6807,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", - "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.0-next.1" @@ -6834,6 +7169,21 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, + "side-channel": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.2.tgz", + "integrity": "sha512-7rL9YlPHg7Ancea1S96Pa8/QWb4BtXL/TZvS6B8XFetGBeuhAsfmUspK6DokBeZ64+Kj9TCNRD/30pVz1BvQNA==", + "requires": { + "es-abstract": "^1.17.0-next.1", + "object-inspect": "^1.7.0" + } + }, + "sigmund": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", + "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", + "dev": true + }, "signal-exit": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", @@ -7105,11 +7455,23 @@ "strip-ansi": "^3.0.0" } }, + "string.prototype.matchall": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.2.tgz", + "integrity": "sha512-N/jp6O5fMf9os0JU3E72Qhf590RSRZU/ungsL/qJUYVTNv7hTG0P/dbPjxINVN9jpscu3nzYwKESU3P3RY5tOg==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0", + "has-symbols": "^1.0.1", + "internal-slot": "^1.0.2", + "regexp.prototype.flags": "^1.3.0", + "side-channel": "^1.0.2" + } + }, "string.prototype.trimleft": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.1.tgz", "integrity": "sha512-iu2AGd3PuP5Rp7x2kEZCrB2Nf41ehzh+goo8TV7z8/XDBbsvc6HQIlUl9RjkZ4oyrW1XM5UwlGl1oVEaDjg6Ag==", - "dev": true, "requires": { "define-properties": "^1.1.3", "function-bind": "^1.1.1" @@ -7119,7 +7481,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.1.tgz", "integrity": "sha512-qFvWL3/+QIgZXVmJBfpHmxLB7xsUXz6HsUmP8+5dRaC3Q7oKUv9Vo6aMCRZC1smrtyECFsIT30PqBJ1gTjAs+g==", - "dev": true, "requires": { "define-properties": "^1.1.3", "function-bind": "^1.1.1" @@ -7218,7 +7579,6 @@ "version": "7.1.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, "requires": { "has-flag": "^4.0.0" }, @@ -7226,8 +7586,7 @@ "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" } } }, @@ -7769,6 +8128,43 @@ "latest-version": "^5.0.0", "semver-diff": "^2.0.0", "xdg-basedir": "^3.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } } }, "uri-js": { diff --git a/package.json b/package.json index eb47b05ee..9154063aa 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ ], "sources": [ "lib/**/*.js", + "lib/validation/schema/**/*.json", "test/lib/**/*.js" ] }, @@ -100,6 +101,10 @@ "@ui5/builder": "^1.10.1", "@ui5/logger": "^1.0.2", "@ui5/server": "^1.6.0", + "ajv": "^6.12.0", + "ajv-errors": "^1.0.1", + "chalk": "^3.0.0", + "escape-string-regexp": "^2.0.0", "graceful-fs": "^4.2.3", "js-yaml": "^3.13.1", "libnpmconfig": "^1.2.1", @@ -109,7 +114,8 @@ "pretty-hrtime": "^1.0.3", "read-pkg": "^3.0.0", "read-pkg-up": "^4.0.0", - "resolve": "^1.15.1" + "resolve": "^1.15.1", + "string.prototype.matchall": "^4.0.2" }, "devDependencies": { "ava": "^2.4.0", @@ -120,6 +126,11 @@ "eslint": "^5.16.0", "eslint-config-google": "^0.14.0", "eslint-plugin-jsdoc": "^4.8.4", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^4.0.1", + "istanbul-lib-report": "^3.0.0", + "istanbul-reports": "^3.0.0", + "js-beautify": "^1.10.3", "jsdoc": "^3.6.3", "mock-require": "^3.0.3", "nyc": "^15.0.0", diff --git a/test/lib/extensions.js b/test/lib/extensions.js index c518efe9e..acb9e1de5 100644 --- a/test/lib/extensions.js +++ b/test/lib/extensions.js @@ -1,6 +1,7 @@ const test = require("ava"); const path = require("path"); const sinon = require("sinon"); +const ValidationError = require("../../lib/validation/ValidationError"); const projectPreprocessor = require("../..").projectPreprocessor; const Preprocessor = require("../..").projectPreprocessor._ProjectPreprocessor; const applicationAPath = path.join(__dirname, "..", "fixtures", "application.a"); @@ -112,6 +113,50 @@ test("Project with project-shim extension with dependency configuration", (t) => }); }); +test("Project with project-shim extension with invalid dependency configuration", async (t) => { + const tree = { + id: "application.a", + path: applicationAPath, + dependencies: [{ + id: "extension.a", + path: applicationAPath, + dependencies: [], + version: "1.0.0", + specVersion: "0.1", + kind: "extension", + type: "project-shim", + metadata: { + name: "shims.a" + }, + shims: { + configurations: { + "legacy.library.a": { + specVersion: "2.0", + type: "library" + } + } + } + }, { + id: "legacy.library.a", + version: "1.0.0", + path: legacyLibraryAPath, + dependencies: [] + }], + version: "1.0.0", + specVersion: "0.1", + type: "application", + metadata: { + name: "xy" + } + }; + + const validationError = await t.throwsAsync(projectPreprocessor.processTree(tree), { + instanceOf: ValidationError + }); + t.true(validationError.message.includes("Configuration should have required property 'metadata'"), + "ValidationError should contain error about missing metadata configuration"); +}); + test("Project with project-shim extension with dependency declaration and configuration", (t) => { const tree = { id: "application.a", diff --git a/test/lib/projectPreprocessor.js b/test/lib/projectPreprocessor.js index be02538a6..426a5dc84 100644 --- a/test/lib/projectPreprocessor.js +++ b/test/lib/projectPreprocessor.js @@ -2,6 +2,9 @@ const test = require("ava"); const sinon = require("sinon"); const mock = require("mock-require"); const path = require("path"); +const gracefulFs = require("graceful-fs"); +const validator = require("../../lib/validation/validator"); +const ValidationError = require("../../lib/validation/ValidationError"); const projectPreprocessor = require("../../lib/projectPreprocessor"); const applicationAPath = path.join(__dirname, "..", "fixtures", "application.a"); const applicationBPath = path.join(__dirname, "..", "fixtures", "application.b"); @@ -169,15 +172,16 @@ test("Project with ui5.yaml at default location and some configuration", (t) => }); }); -test("Missing configuration for root project", (t) => { +test("Missing configuration for root project", async (t) => { const tree = { id: "application.a", path: "non-existent", dependencies: [] }; - return t.throwsAsync(projectPreprocessor.processTree(tree), - "No specification version defined for root project application.a", - "Rejected with error"); + const exception = await t.throwsAsync(projectPreprocessor.processTree(tree)); + + t.true(exception.message.includes("Failed to read configuration for project application.a"), + "Error message should contain expected reason"); }); test("Missing id for root project", (t) => { @@ -1586,7 +1590,7 @@ test("Library version in package.json data is missing", (t) => { }); }); -test("specVersion: Missing version", (t) => { +test("specVersion: Missing version", async (t) => { const tree = { id: "application.a", path: "non-existent", @@ -1597,9 +1601,10 @@ test("specVersion: Missing version", (t) => { name: "xy" } }; - return t.throwsAsync(projectPreprocessor.processTree(tree), - "No specification version defined for root project application.a", - "Rejected with error"); + const exception = await t.throwsAsync(projectPreprocessor.processTree(tree)); + + t.true(exception.message.includes("Failed to read configuration for project application.a"), + "Error message should contain expected reason"); }); test("specVersion: Project with invalid version", async (t) => { @@ -1614,11 +1619,12 @@ test("specVersion: Project with invalid version", async (t) => { name: "xy" } }; - await t.throwsAsync(projectPreprocessor.processTree(tree), - "Unsupported specification version 0.9 defined for project application.a. " + - "Your UI5 CLI installation might be outdated. For details see " + - "https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions", - "Rejected with error"); + const validationError = await t.throwsAsync(projectPreprocessor.processTree(tree), { + instanceOf: ValidationError + }); + + t.is(validationError.errors.length, 1, "ValidationError should have one error object"); + t.is(validationError.errors[0].dataPath, "/specVersion", "Error should be for the specVersion"); }); test("specVersion: Project with valid version 0.1", async (t) => { @@ -2028,3 +2034,251 @@ test.serial("checkProjectMetadata: No warning logged for nested SAP internal lib t.is(logWarnSpy.callCount, 0, "No warning got logged"); }); + + +test.serial("readConfigFile: No exception for valid config", async (t) => { + const configPath = path.join("/application", "ui5.yaml"); + const ui5yaml = ` +--- +specVersion: "2.0" +type: application +metadata: + name: application.a +`; + + const validateSpy = sinon.spy(validator, "validate"); + + sinon.stub(gracefulFs, "readFile") + .callsFake((path) => { + throw new Error("readFileStub called with unexpected path: " + path); + }) + .withArgs(configPath).yieldsAsync(null, ui5yaml); + + // Re-require tested module + const projectPreprocessor = mock.reRequire("../../lib/projectPreprocessor"); + const preprocessor = new projectPreprocessor._ProjectPreprocessor({}); + + await t.notThrowsAsync(async () => { + await preprocessor.readConfigFile({path: "/application", id: "id"}); + }); + + t.is(validateSpy.callCount, 1, "validate should be called once"); + t.deepEqual(validateSpy.getCall(0).args, [{ + config: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + } + }, + project: { + id: "id", + }, + yaml: { + documentIndex: 0, + path: configPath, + source: ui5yaml + }, + + }], + "validate should be called with expected args"); +}); + +test.serial("readConfigFile: Exception for invalid config", async (t) => { + const configPath = path.join("/application", "ui5.yaml"); + const ui5yaml = ` +--- +specVersion: "2.0" +type: application +metadata: + name: application.a +--- +specVersion: "2.0" +kind: extension +type: task +metadata: + name: my-task +--- +specVersion: "2.0" +kind: extension +type: server-middleware +metadata: + name: my-middleware +`; + + const validateSpy = sinon.spy(validator, "validate"); + + sinon.stub(gracefulFs, "readFile") + .callsFake((path) => { + throw new Error("readFileStub called with unexpected path: " + path); + }) + .withArgs(configPath).yieldsAsync(null, ui5yaml); + + // Re-require tested module + const projectPreprocessor = mock.reRequire("../../lib/projectPreprocessor"); + const preprocessor = new projectPreprocessor._ProjectPreprocessor({}); + + const validationError = await t.throwsAsync(async () => { + await preprocessor.readConfigFile({path: "/application", id: "id"}); + }, { + instanceOf: ValidationError, + name: "ValidationError" + }); + + t.is(validationError.yaml.documentIndex, 1, "Error of first invalid document should be thrown"); + + t.is(validateSpy.callCount, 3, "validate should be called 3 times"); + t.deepEqual(validateSpy.getCall(0).args, [{ + config: { + specVersion: "2.0", + type: "application", + metadata: { + name: "application.a" + } + }, + project: { + id: "id", + }, + yaml: { + documentIndex: 0, + path: configPath, + source: ui5yaml, + }, + }], + "validate should be called first time with expected args"); + t.deepEqual(validateSpy.getCall(1).args, [{ + config: { + specVersion: "2.0", + kind: "extension", + type: "task", + metadata: { + name: "my-task" + } + }, + project: { + id: "id", + }, + yaml: { + documentIndex: 1, + path: configPath, + source: ui5yaml, + }, + }], + "validate should be called second time with expected args"); + t.deepEqual(validateSpy.getCall(2).args, [{ + config: { + specVersion: "2.0", + kind: "extension", + type: "server-middleware", + metadata: { + name: "my-middleware" + } + }, + project: { + id: "id", + }, + yaml: { + documentIndex: 2, + path: configPath, + source: ui5yaml, + }, + }], + "validate should be called third time with expected args"); +}); + +test.serial("readConfigFile: Exception for invalid YAML file", async (t) => { + const configPath = path.join("/application", "ui5.yaml"); + const ui5yaml = ` +-- +specVersion: "2.0" +foo: bar +metadata: + name: application.a +`; + + const validateSpy = sinon.spy(validator, "validate"); + + sinon.stub(gracefulFs, "readFile") + .callsFake((path) => { + throw new Error("readFileStub called with unexpected path: " + path); + }) + .withArgs(configPath).yieldsAsync(null, ui5yaml); + + // Re-require tested module + const projectPreprocessor = mock.reRequire("../../lib/projectPreprocessor"); + const preprocessor = new projectPreprocessor._ProjectPreprocessor({}); + + const error = await t.throwsAsync(async () => { + await preprocessor.readConfigFile({path: "/application", id: "my-project"}); + }); + + t.true(error.message.includes("Failed to parse configuration for project my-project"), + "Error message should contain information about parsing error"); + + t.is(validateSpy.callCount, 0, "validate should not be called"); +}); + +test.serial("readConfigFile: Empty YAML", async (t) => { + const configPath = path.join("/application", "ui5.yaml"); + const ui5yaml = ""; + + const validateSpy = sinon.spy(validator, "validate"); + + sinon.stub(gracefulFs, "readFile") + .callsFake((path) => { + throw new Error("readFileStub called with unexpected path: " + path); + }) + .withArgs(configPath).yieldsAsync(null, ui5yaml); + + // Re-require tested module + const projectPreprocessor = mock.reRequire("../../lib/projectPreprocessor"); + const preprocessor = new projectPreprocessor._ProjectPreprocessor({}); + + const configs = await preprocessor.readConfigFile({path: "/application", id: "my-project"}); + + t.deepEqual(configs, [], "Empty YAML should result in empty array"); + t.is(validateSpy.callCount, 0, "validate should not be called"); +}); + +test.serial("loadProjectConfiguration: Runs validation if specVersion already exists (error)", async (t) => { + const config = { + specVersion: "2.0", + foo: "bar", + metadata: { + name: "application.a" + }, + + id: "id", + version: "1.0.0", + path: "path", + dependencies: [] + }; + + const validateSpy = sinon.spy(validator, "validate"); + + // Re-require tested module + const projectPreprocessor = mock.reRequire("../../lib/projectPreprocessor"); + const preprocessor = new projectPreprocessor._ProjectPreprocessor({}); + + await t.throwsAsync(async () => { + await preprocessor.loadProjectConfiguration(config); + }, { + instanceOf: ValidationError, + name: "ValidationError" + }); + + t.is(validateSpy.callCount, 1, "validate should be called once"); + t.deepEqual(validateSpy.getCall(0).args, [{ + config: { + specVersion: "2.0", + foo: "bar", + metadata: { + name: "application.a" + } + }, + project: { + id: "id" + } + }], + "validate should be called with expected args"); +}); diff --git a/test/lib/translators/ui5Framework.integration.js b/test/lib/translators/ui5Framework.integration.js index 6614c7f93..3d0ac756a 100644 --- a/test/lib/translators/ui5Framework.integration.js +++ b/test/lib/translators/ui5Framework.integration.js @@ -175,143 +175,146 @@ function defineTest(testName, { sinon.stub(normalizer, "generateDependencyTree").resolves(translatorTree); sinon.stub(projectPreprocessor._ProjectPreprocessor.prototype, "readConfigFile") - .callsFake(async (configPath) => { - throw new Error("ProjectPreprocessor#readConfigFile stub called with unknown configPath: " + configPath); - }) - .withArgs(path.join(fakeBaseDir, "project-test-application", "ui5.yaml")) - .resolves([{ - specVersion: "2.0", - type: "application", - metadata: { - name: "test-application" - }, - framework: { - name: frameworkName, - version: "1.75.0", - libraries: [ - { - name: "sap.ui.lib1" + .callsFake(async (project) => { + switch (project.path) { + case path.join(fakeBaseDir, "project-test-application"): + return [{ + specVersion: "2.0", + type: "application", + metadata: { + name: "test-application" }, - { - name: "sap.ui.lib4", - optional: true + framework: { + name: frameworkName, + version: "1.75.0", + libraries: [ + { + name: "sap.ui.lib1" + }, + { + name: "sap.ui.lib4", + optional: true + }, + { + name: "sap.ui.lib8", + development: true + } + ] + } + }]; + case path.join(fakeBaseDir, "project-test-dependency"): + return [{ + specVersion: "2.0", + type: "library", + metadata: { + name: "test-dependency" }, - { - name: "sap.ui.lib8", - development: true + framework: { + version: "1.99.0", + name: frameworkName, + libraries: [ + { + name: "sap.ui.lib1" + }, + { + name: "sap.ui.lib2" + }, + { + name: "sap.ui.lib5", + optional: true + }, + { + name: "sap.ui.lib6", + development: true + }, + { + name: "sap.ui.lib8", + // optional dependency gets resolved by dev-dependency of root project + optional: true + } + ] } - ] - } - }]) - .withArgs(path.join(fakeBaseDir, "project-test-dependency", "ui5.yaml")) - .resolves([{ - specVersion: "2.0", - type: "library", - metadata: { - name: "test-dependency" - }, - framework: { - version: "1.99.0", - libraries: [ - { + }]; + case path.join(fakeBaseDir, "project-test-dependency-no-framework"): + return [{ + specVersion: "2.0", + type: "library", + metadata: { + name: "test-dependency-no-framework" + } + }]; + case path.join(fakeBaseDir, "project-test-dependency-framework-old-spec-version"): + return [{ + specVersion: "1.1", + type: "library", + metadata: { + name: "test-dependency-framework-old-spec-version" + }, + framework: { + libraries: [ + { + name: "sap.ui.lib5" + } + ] + } + }]; + case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib1", + frameworkName === "SAPUI5" ? "1.75.1" : "1.75.0"): + return [{ + specVersion: "1.0", + type: "library", + metadata: { name: "sap.ui.lib1" }, - { + framework: {libraries: []} + }]; + case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib2", + frameworkName === "SAPUI5" ? "1.75.2" : "1.75.0"): + return [{ + specVersion: "1.0", + type: "library", + metadata: { name: "sap.ui.lib2" }, - { - name: "sap.ui.lib5", - optional: true + framework: {libraries: []} + }]; + case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib3", + frameworkName === "SAPUI5" ? "1.75.3" : "1.75.0"): + return [{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib3" }, - { - name: "sap.ui.lib6", - development: true + framework: {libraries: []} + }]; + case path.join(ui5PackagesBaseDir, "@openui5", "sap.ui.lib4", + frameworkName === "SAPUI5" ? "1.75.4" : "1.75.0"): + return [{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib4" }, - { - name: "sap.ui.lib8", - optional: true // optional dependency gets resolved by dev-dependency of root project - } - ] - } - }]) - .withArgs(path.join(fakeBaseDir, "project-test-dependency-no-framework", "ui5.yaml")) - .resolves([{ - specVersion: "2.0", - type: "library", - metadata: { - name: "test-dependency-no-framework" - } - }]) - .withArgs(path.join(fakeBaseDir, "project-test-dependency-framework-old-spec-version", "ui5.yaml")) - .resolves([{ - specVersion: "1.1", - type: "library", - metadata: { - name: "test-dependency-framework-old-spec-version" - }, - framework: { - libraries: [ - { - name: "sap.ui.lib5" - } - ] + framework: {libraries: []} + }]; + case path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib8", + frameworkName === "SAPUI5" ? "1.75.8" : "1.75.0"): + return [{ + specVersion: "1.0", + type: "library", + metadata: { + name: "sap.ui.lib8" + }, + framework: {libraries: []} + }]; + default: + throw new Error( + "ProjectPreprocessor#readConfigFile stub called with unknown project: " + + (project && project.path) + ); } - }]) - .withArgs(path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib1", - frameworkName === "SAPUI5" ? "1.75.1" : "1.75.0", "ui5.yaml" - )) - .resolves([{ - specVersion: "1.0", - type: "library", - metadata: { - name: "sap.ui.lib1" - }, - framework: {libraries: []} - }]) - .withArgs(path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib2", - frameworkName === "SAPUI5" ? "1.75.2" : "1.75.0", "ui5.yaml" - )) - .resolves([{ - specVersion: "1.0", - type: "library", - metadata: { - name: "sap.ui.lib2" - }, - framework: {libraries: []} - }]) - .withArgs(path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib3", - frameworkName === "SAPUI5" ? "1.75.3" : "1.75.0", "ui5.yaml" - )) - .resolves([{ - specVersion: "1.0", - type: "library", - metadata: { - name: "sap.ui.lib3" - }, - framework: {libraries: []} - }]) - .withArgs(path.join(ui5PackagesBaseDir, "@openui5", "sap.ui.lib4", - frameworkName === "SAPUI5" ? "1.75.4" : "1.75.0", "ui5.yaml" - )) - .resolves([{ - specVersion: "1.0", - type: "library", - metadata: { - name: "sap.ui.lib4" - }, - framework: {libraries: []} - }]) - .withArgs(path.join(ui5PackagesBaseDir, npmScope, "sap.ui.lib8", - frameworkName === "SAPUI5" ? "1.75.8" : "1.75.0", "ui5.yaml" - )) - .resolves([{ - specVersion: "1.0", - type: "library", - metadata: { - name: "sap.ui.lib8" - }, - framework: {libraries: []} - }]); + }); // Prevent applying types as this would require a lot of mocking sinon.stub(projectPreprocessor._ProjectPreprocessor.prototype, "applyType"); @@ -401,6 +404,7 @@ function defineTest(testName, { type: "library", framework: { version: "1.99.0", + name: frameworkName, libraries: [ { name: "sap.ui.lib1" @@ -534,30 +538,36 @@ function defineErrorTest(testName, { sinon.stub(normalizer, "generateDependencyTree").resolves(translatorTree); sinon.stub(projectPreprocessor._ProjectPreprocessor.prototype, "readConfigFile") - .callsFake(async (configPath) => { - throw new Error("ProjectPreprocessor#readConfigFile stub called with unknown configPath: " + configPath); - }) - .withArgs(path.join(fakeBaseDir, "application-project", "ui5.yaml")) - .resolves([{ - specVersion: "2.0", - type: "application", - metadata: { - name: "test-project" - }, - framework: { - name: frameworkName, - version: "1.75.0", - libraries: [ - { - name: "sap.ui.lib1" + .callsFake(async (project) => { + switch (project.path) { + case path.join(fakeBaseDir, "application-project"): + return [{ + specVersion: "2.0", + type: "application", + metadata: { + name: "test-project" }, - { - name: "sap.ui.lib4", - optional: true + framework: { + name: frameworkName, + version: "1.75.0", + libraries: [ + { + name: "sap.ui.lib1" + }, + { + name: "sap.ui.lib4", + optional: true + } + ] } - ] + }]; + default: + throw new Error( + "ProjectPreprocessor#readConfigFile stub called with unknown project: " + + (project && project.path) + ); } - }]); + }); // Prevent applying types as this would require a lot of mocking sinon.stub(projectPreprocessor._ProjectPreprocessor.prototype, "applyType"); diff --git a/test/lib/validation/ValidationError.js b/test/lib/validation/ValidationError.js new file mode 100644 index 000000000..25d69f4dd --- /dev/null +++ b/test/lib/validation/ValidationError.js @@ -0,0 +1,938 @@ +const test = require("ava"); +const sinon = require("sinon"); +const chalk = require("chalk"); + +const ValidationError = require("../../../lib/validation/ValidationError"); + +test.afterEach.always((t) => { + sinon.restore(); +}); + +test.serial("ValidationError constructor", (t) => { + const errors = [ + {dataPath: "", keyword: "", message: "error1", params: {}}, + {dataPath: "", keyword: "", message: "error2", params: {}} + ]; + const project = {id: "id"}; + const schema = {schema: "schema"}; + const data = {data: "data"}; + const yaml = {path: "path", source: "source", documentIndex: 0}; + + const filteredErrors = [{dataPath: "", keyword: "", message: "error1", params: {}}]; + + const filterErrorsStub = sinon.stub(ValidationError, "filterErrors"); + filterErrorsStub.returns(filteredErrors); + + const formatErrorsStub = sinon.stub(ValidationError.prototype, "formatErrors"); + formatErrorsStub.returns("Formatted Message"); + + const validationError = new ValidationError({errors, schema, data, project, yaml}); + + t.true(validationError instanceof ValidationError, "ValidationError constructor returns instance"); + t.true(validationError instanceof Error, "ValidationError inherits from Error"); + t.is(validationError.name, "ValidationError", "ValidationError should have 'name' property"); + + t.deepEqual(validationError.errors, filteredErrors, + "ValidationError should have 'errors' property with filtered errors"); + t.deepEqual(validationError.project, project, "ValidationError should have 'project' property"); + t.deepEqual(validationError.yaml, yaml, "≈ should have 'yaml' property"); + t.is(validationError.message, "Formatted Message", "ValidationError should have 'message' property"); + + t.is(filterErrorsStub.callCount, 1, "ValidationError.filterErrors should be called once"); + t.deepEqual(filterErrorsStub.getCall(0).args, [errors], + "ValidationError.filterErrors should be called with errors, project and yaml"); + + t.is(formatErrorsStub.callCount, 1, "ValidationError#formatErrors should be called once"); + t.deepEqual(formatErrorsStub.getCall(0).args, [], + "ValidationError.formatErrors should be called without args"); +}); + +test.serial("ValidationError.filterErrors", (t) => { + const allErrors = [ + { + keyword: "if" + }, + { + dataPath: "dataPath1", + keyword: "keyword1" + }, + { + dataPath: "dataPath1", + keyword: "keyword2" + }, + { + dataPath: "dataPath3", + keyword: "keyword2" + }, + { + dataPath: "dataPath1", + keyword: "keyword1" + }, + { + dataPath: "dataPath1", + keyword: "keyword1", + params: { + type: "foo" + } + }, + { + dataPath: "dataPath4", + keyword: "keyword5", + params: { + type: "foo" + } + }, + { + dataPath: "dataPath6", + keyword: "keyword6", + params: { + errors: [ + { + "type": "foo" + }, + { + "type": "bar" + } + ] + } + }, + { + dataPath: "dataPath6", + keyword: "keyword6", + params: { + errors: [ + { + "type": "foo" + }, + { + "type": "bar" + } + ] + } + }, + { + dataPath: "dataPath6", + keyword: "keyword6", + params: { + errors: [ + { + "type": "foo" + }, + { + "type": "foo" + } + ] + } + } + ]; + + const expectedErrors = [ + { + dataPath: "dataPath1", + keyword: "keyword1" + }, + { + dataPath: "dataPath1", + keyword: "keyword2" + }, + { + dataPath: "dataPath3", + keyword: "keyword2" + }, + { + dataPath: "dataPath1", + keyword: "keyword1", + params: { + type: "foo" + } + }, + { + dataPath: "dataPath4", + keyword: "keyword5", + params: { + type: "foo" + } + }, + { + dataPath: "dataPath6", + keyword: "keyword6", + params: { + errors: [ + { + "type": "foo" + }, + { + "type": "bar" + } + ] + } + }, + { + dataPath: "dataPath6", + keyword: "keyword6", + params: { + errors: [ + { + "type": "foo" + }, + { + "type": "foo" + } + ] + } + } + ]; + + const filteredErrors = ValidationError.filterErrors(allErrors); + + t.deepEqual(filteredErrors, expectedErrors, "filterErrors should return expected errors"); +}); + +test.serial("ValidationError.formatErrors", (t) => { + const fakeValidationErrorInstance = { + errors: [{}, {}], + project: {id: "my-project"} + }; + + const formatErrorStub = sinon.stub(); + formatErrorStub.onFirstCall().returns("Error message 1"); + formatErrorStub.onSecondCall().returns("Error message 2"); + fakeValidationErrorInstance.formatError = formatErrorStub; + + const message = ValidationError.prototype.formatErrors.apply(fakeValidationErrorInstance); + + const expectedMessage = +`${chalk.red("Invalid ui5.yaml configuration for project my-project")} + +Error message 1 + +${process.stdout.isTTY ? chalk.grey.dim("─".repeat(process.stdout.columns || 80)) : ""} + +Error message 2`; + + t.is(message, expectedMessage); + + t.is(formatErrorStub.callCount, 2, "formatErrorStub should be called twice"); + t.deepEqual(formatErrorStub.getCall(0).args, [ + fakeValidationErrorInstance.errors[0] + ], "formatErrorStub should be called with first error"); + t.deepEqual(formatErrorStub.getCall(1).args, [ + fakeValidationErrorInstance.errors[1] + ], "formatErrorStub should be called with second error"); +}); + +test.serial("ValidationError.formatError (with yaml)", (t) => { + const fakeValidationErrorInstance = { + yaml: { + path: "/path", + source: "source" + } + }; + const error = {"error": true}; + + const formatMessageStub = sinon.stub(ValidationError, "formatMessage"); + formatMessageStub.returns("First line\nSecond line\nThird line"); + + const getYamlExtractStub = sinon.stub(ValidationError, "getYamlExtract"); + getYamlExtractStub.returns("YAML"); + + const message = ValidationError.prototype.formatError.call(fakeValidationErrorInstance, error); + + const expectedMessage = +`First line + +YAML +Second line +Third line`; + + t.is(message, expectedMessage); + + t.is(formatMessageStub.callCount, 1, "formatMessageStub should be called once"); + t.deepEqual(formatMessageStub.getCall(0).args, [error], "formatMessageStub should be called with error"); + + t.is(getYamlExtractStub.callCount, 1, "getYamlExtractStub should be called once"); + t.deepEqual(getYamlExtractStub.getCall(0).args, [ + {error, yaml: fakeValidationErrorInstance.yaml}], + "getYamlExtractStub should be called with error and yaml"); +}); + +test.serial("ValidationError.getYamlExtract", (t) => { + const error = {}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property1: value1 +property2: value2 +property3: value3 +property4: value4 +property5: value5 +`, + documentIndex: 0 + }; + + const analyzeYamlErrorStub = sinon.stub(ValidationError, "analyzeYamlError"); + analyzeYamlErrorStub.returns({line: 3, column: 12}); + + const expectedYamlExtract = + chalk.grey("/my-project/ui5.yaml:3") + + "\n\n" + + chalk.grey("1:") + " property1: value1\n" + + chalk.grey("2:") + " property2: value2\n" + + chalk.bgRed(chalk.grey("3:") + " property3: value3\n") + + " ".repeat(14) + chalk.red("^"); + + const yamlExtract = ValidationError.getYamlExtract({error, yaml}); + + t.is(yamlExtract, expectedYamlExtract); +}); + +test.serial("ValidationError.getSourceExtract", (t) => { + const yamlSource = +`property1: value1 +property2: value2 +`; + const line = 2; + const column = 1; + + const expected = + chalk.grey("1:") + " property1: value1\n" + + chalk.bgRed(chalk.grey("2:") + " property2: value2\n") + + " ".repeat(3) + chalk.red("^"); + + const sourceExtract = ValidationError.getSourceExtract(yamlSource, line, column); + + t.is(sourceExtract, expected, "getSourceExtract should return expected string"); +}); + +test.serial("ValidationError.getSourceExtract (Windows Line-Endings)", (t) => { + const yamlSource = +"property1: value1\r\n" + +"property2: value2\r\n"; + const line = 2; + const column = 1; + + const expected = + chalk.grey("1:") + " property1: value1\n" + + chalk.bgRed(chalk.grey("2:") + " property2: value2\n") + + " ".repeat(3) + chalk.red("^"); + + const sourceExtract = ValidationError.getSourceExtract(yamlSource, line, column); + + t.is(sourceExtract, expected, "getSourceExtract should return expected string"); +}); + +test.serial("ValidationError.analyzeYamlError: Property", (t) => { + const error = {dataPath: "/property3"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property1: value1 +property2: value2 +property3: value3 +property4: value4 +property5: value5 +`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 3, column: 1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Nested property", (t) => { + const error = {dataPath: "/property2/property3"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property1: value1 +property2: + property3: value3 +property3: value3 +`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 3, column: 3}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Array", (t) => { + const error = {dataPath: "/property/list/2/name"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property: + list: + - name: ' - - - - -' + - name: other - name- with- hyphens + - name: name3 +`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 5, column: 7}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Nested array", (t) => { + const error = {dataPath: "/items/2/subItems/1"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`items: + - subItems: + - foo + - bar + - subItems: + - foo + - bar + - subItems: + - foo + - bar +`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 10, column: 7}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Nested array (Windows Line-Endings)", (t) => { + const error = {dataPath: "/items/2/subItems/1"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +"items:\r\n" + +" - subItems:\r\n" + +" - foo\r\n" + +" - bar\r\n" + +" - subItems:\r\n" + +" - foo\r\n" + +" - bar\r\n" + +" - subItems:\r\n" + +" - foo\r\n" + +" - bar\r\n", + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 10, column: 7}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Array with square brackets (not supported)", (t) => { + const error = {dataPath: "/items/2"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`items: [1, 2, 3] +`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: -1, column: -1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Multiline array with square brackets (not supported)", (t) => { + const error = {dataPath: "/items/2"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`items: [ + 1, + 2, + 3 +] +`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: -1, column: -1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Nested property with comments", (t) => { + const error = {dataPath: "/property1/property2/property3/property4"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property1: + property2: + property3: + # property4: value4444 + property4: value4 +`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 5, column: 7}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Nested properties with same name", (t) => { + const error = {dataPath: "/property/property/property/property"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property: + property: + property: + # property: foo + property: bar +`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 5, column: 7}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Error keyword=required, no dataPath", (t) => { + const error = {dataPath: "", keyword: "required"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: ``, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: -1, column: -1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Error keyword=required", (t) => { + const error = {dataPath: "/property2", keyword: "required"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property1: true +property2: + property3: true +`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 2, column: 1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Error keyword=additionalProperties", (t) => { + const error = { + dataPath: "/property2", + keyword: "additionalProperties", + params: { + additionalProperty: "property3" + } + }; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property1: true +property2: + property3: true +`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 3, column: 3}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: documentIndex=0 (Without leading separator)", (t) => { + const error = {dataPath: "/property3"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property1: value1document1 +property2: value2document1 +property3: value3document1 +property4: value4document1 +property5: value5document1 +--- +property1: value1document2 +property2: value2document2 +property3: value3document2 +property4: value4document2 +property5: value5document2`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 3, column: 1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: documentIndex=0 (With leading separator)", (t) => { + const error = {dataPath: "/property3"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`--- +property1: value1document1 +property2: value2document1 +property3: value3document1 +property4: value4document1 +property5: value5document1 +--- +property1: value1document2 +property2: value2document2 +property3: value3document2 +property4: value4document2 +property5: value5document2`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 4, column: 1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: documentIndex=0 (With leading separator and empty lines)", (t) => { + const error = {dataPath: "/property3"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +` + + + +--- +property1: value1document1 +property2: value2document1 +property3: value3document1 +property4: value4document1 +property5: value5document1 +--- +property1: value1document2 +property2: value2document2 +property3: value3document2 +property4: value4document2 +property5: value5document2`, + documentIndex: 0 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 8, column: 1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: documentIndex=2 (Without leading separator)", (t) => { + const error = {dataPath: "/property3"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property1: value1document1 +property2: value2document1 +property3: value3document1 +property4: value4document1 +property5: value5document1 +--- +property1: value1document2 +property2: value2document2 +property3: value3document2 +property4: value4document2 +property5: value5document2 +--- +property1: value1document3 +property2: value2document3 +property3: value3document3 +property4: value4document3 +property5: value5document3 +`, + documentIndex: 2 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 15, column: 1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: documentIndex=2 (With leading separator)", (t) => { + const error = {dataPath: "/property3"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`--- +property1: value1document1 +property2: value2document1 +property3: value3document1 +property4: value4document1 +property5: value5document1 +--- +property1: value1document2 +property2: value2document2 +property3: value3document2 +property4: value4document2 +property5: value5document2 +--- +property1: value1document3 +property2: value2document3 +property3: value3document3 +property4: value4document3 +property5: value5document3 +`, + documentIndex: 2 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 16, column: 1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: documentIndex=2 (With leading separator and empty lines)", (t) => { + const error = {dataPath: "/property3"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +` + + + + +--- +property1: value1document1 +property2: value2document1 +property3: value3document1 +property4: value4document1 +property5: value5document1 +--- +property1: value1document2 +property2: value2document2 +property3: value3document2 +property4: value4document2 +property5: value5document2 +--- +property1: value1document3 +property2: value2document3 +property3: value3document3 +property4: value4document3 +property5: value5document3 +`, + documentIndex: 2 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: 21, column: 1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Invalid documentIndex=1 (With leading separator)", (t) => { + const error = {dataPath: "/property3"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`--- +property1: value1document1 +property2: value2document1 +property3: value3document1 +property4: value4document1 +property5: value5document1 +`, + documentIndex: 1 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: -1, column: -1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.analyzeYamlError: Invalid documentIndex=1 (Without leading separator)", (t) => { + const error = {dataPath: "/property3"}; + const yaml = { + path: "/my-project/ui5.yaml", + source: +`property1: value1document1 +property2: value2document1 +property3: value3document1 +property4: value4document1 +property5: value5document1 +`, + documentIndex: 1 + }; + + const info = ValidationError.analyzeYamlError({error, yaml}); + + t.deepEqual(info, {line: -1, column: -1}, + "analyzeYamlError should return expected results"); +}); + +test.serial("ValidationError.formatMessage: keyword=type dataPath=", (t) => { + const error = { + dataPath: "", + keyword: "type", + message: "should be object", + params: { + type: "object", + }, + schemaPath: "#/type", + }; + + const expectedErrorMessage = "Configuration should be of type 'object'"; + + const errorMessage = ValidationError.formatMessage(error); + t.is(errorMessage, expectedErrorMessage); +}); + +test.serial("ValidationError.formatMessage: keyword=type", (t) => { + const error = { + dataPath: "/foo", + keyword: "type", + message: "should be object", + params: { + type: "object", + }, + schemaPath: "#/type", + }; + + const expectedErrorMessage = `Configuration ${chalk.underline(chalk.red("foo"))} should be of type 'object'`; + + const errorMessage = ValidationError.formatMessage(error); + t.is(errorMessage, expectedErrorMessage); +}); + +test.serial("ValidationError.formatMessage: keyword=required w/o dataPath", (t) => { + const error = { + dataPath: "", + keyword: "required", + message: "should have required property 'specVersion'", + params: { + missingProperty: "specVersion", + }, + schemaPath: "#/required", + }; + + const expectedErrorMessage = "Configuration should have required property 'specVersion'"; + + const errorMessage = ValidationError.formatMessage(error); + t.is(errorMessage, expectedErrorMessage); +}); + +test.serial("ValidationError.formatMessage: keyword=required", (t) => { + const error = { + keyword: "required", + dataPath: "/metadata", + schemaPath: "../ui5.json#/definitions/metadata/required", + params: {missingProperty: "name"}, + message: "should have required property 'name'" + }; + + const expectedErrorMessage = `Configuration ${chalk.underline(chalk.red("metadata"))} should have required property 'name'`; + + const errorMessage = ValidationError.formatMessage(error); + t.is(errorMessage, expectedErrorMessage); +}); + +test.serial("ValidationError.formatMessage: keyword=errorMessage", (t) => { + const error = { + dataPath: "/specVersion", + keyword: "errorMessage", + message: +`Unsupported "specVersion" +Your UI5 CLI installation might be outdated. +Supported specification versions: "2.0", "1.1", "1.0", "0.1" +For details see: https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`, + params: { + errors: [ + { + dataPath: "/specVersion", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "2.0", + "1.1", + "1.0", + "0.1", + ], + }, + schemaPath: "#/properties/specVersion/enum", + }, + ], + }, + schemaPath: "#/properties/specVersion/errorMessage", + }; + + const expectedErrorMessage = +`Unsupported "specVersion" +Your UI5 CLI installation might be outdated. +Supported specification versions: "2.0", "1.1", "1.0", "0.1" +For details see: https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`; + + const errorMessage = ValidationError.formatMessage(error, {}); + t.is(errorMessage, expectedErrorMessage); +}); + +test.serial("ValidationError.formatMessage: keyword=additionalProperties", (t) => { + const error = { + keyword: "additionalProperties", + dataPath: "/resources/configuration", + schemaPath: "#/properties/configuration/additionalProperties", + params: {additionalProperty: "propertiesFileEncoding"}, + message: "should NOT have additional properties" + }; + + const expectedErrorMessage = +`Configuration ${chalk.underline(chalk.red("resources/configuration"))} property propertiesFileEncoding is not expected to be here`; + + const errorMessage = ValidationError.formatMessage(error); + t.is(errorMessage, expectedErrorMessage); +}); + +test.serial("ValidationError.formatMessage: keyword=enum", (t) => { + const error = { + keyword: "enum", + dataPath: "/type", + schemaPath: "#/properties/type/enum", + params: { + allowedValues: ["application", "library", "theme-library", "module"] + }, + message: "should be equal to one of the allowed values" + }; + + const expectedErrorMessage = +`Configuration ${chalk.underline(chalk.red("type"))} should be equal to one of the allowed values +Allowed values: application, library, theme-library, module`; + + const errorMessage = ValidationError.formatMessage(error); + t.is(errorMessage, expectedErrorMessage); +}); + +// test.serial.skip("ValidationError.formatMessage: keyword=pattern", (t) => { +// const error = {}; + +// const expectedErrorMessage = +// ``; + +// const errorMessage = ValidationError.formatMessage(error); +// t.is(errorMessage, expectedErrorMessage); +// }); diff --git a/test/lib/validation/schema/specVersion/2.0/kind/extension.js b/test/lib/validation/schema/specVersion/2.0/kind/extension.js new file mode 100644 index 000000000..af46f8e76 --- /dev/null +++ b/test/lib/validation/schema/specVersion/2.0/kind/extension.js @@ -0,0 +1,152 @@ +const test = require("ava"); +const AjvCoverage = require("../../../../../../utils/AjvCoverage"); +const {_Validator: Validator} = require("../../../../../../../lib/validation/validator"); +const ValidationError = require("../../../../../../../lib/validation/ValidationError"); + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({config, project: {id: "my-project"}}); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError" + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator(); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/specVersion/2.0/kind/extension.json"] + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-extension"}); + const thresholds = { + statements: 80, + branches: 70, + functions: 100, + lines: 80 + }; + t.context.ajvCoverage.verify(thresholds); +}); + +test("Type project-shim", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "extension", + "type": "project-shim", + "metadata": { + "name": "my-project-shim" + }, + "shims": {} + }); +}); + +test("Type server-middleware", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "extension", + "type": "server-middleware", + "metadata": { + "name": "my-server-middleware" + }, + "middleware": { + "path": "middleware.js" + } + }); +}); + +test("Type task", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "extension", + "type": "task", + "metadata": { + "name": "my-task" + }, + "task": { + "path": "task.js" + } + }); +}); + +test("No type", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "extension", + "metadata": { + "name": "my-project" + } + }, [{ + dataPath: "", + keyword: "required", + message: "should have required property 'type'", + params: { + missingProperty: "type", + }, + schemaPath: "#/required", + }]); +}); + +test("Invalid type", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "extension", + "type": "foo", + "metadata": { + "name": "my-project" + } + }, [{ + dataPath: "/type", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "task", + "server-middleware", + "project-shim" + ], + }, + schemaPath: "#/properties/type/enum", + }]); +}); + +test("No specVersion", async (t) => { + await assertValidation(t, { + "kind": "extension", + "type": "project-shim", + "metadata": { + "name": "my-library" + }, + "shims": {} + }, [{ + dataPath: "", + keyword: "required", + message: "should have required property 'specVersion'", + params: { + missingProperty: "specVersion", + }, + schemaPath: "#/required", + }]); +}); + +test("No metadata", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "extension", + "type": "project-shim", + "shims": {} + }, [{ + dataPath: "", + keyword: "required", + message: "should have required property 'metadata'", + params: { + missingProperty: "metadata", + }, + schemaPath: "#/required", + }]); +}); diff --git a/test/lib/validation/schema/specVersion/2.0/kind/extension/project-shim.js b/test/lib/validation/schema/specVersion/2.0/kind/extension/project-shim.js new file mode 100644 index 000000000..121bdb180 --- /dev/null +++ b/test/lib/validation/schema/specVersion/2.0/kind/extension/project-shim.js @@ -0,0 +1,164 @@ +const test = require("ava"); +const AjvCoverage = require("../../../../../../../utils/AjvCoverage"); +const {_Validator: Validator} = require("../../../../../../../../lib/validation/validator"); +const ValidationError = require("../../../../../../../../lib/validation/ValidationError"); + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({config, project: {id: "my-project"}}); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError" + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator(); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/specVersion/2.0/kind/extension/project-shim.json"] + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-extension-project-shim"}); + const thresholds = { + statements: 70, + branches: 55, + functions: 100, + lines: 65 + }; + t.context.ajvCoverage.verify(thresholds); +}); + + +test("kind: extension / type: project-shim", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "extension", + "type": "project-shim", + "metadata": { + "name": "my-project-shim" + }, + "shims": { + "configurations": { + "my-dependency": { + "specVersion": "2.0", + "type": "application", + "metadata": { + "name": "my-application" + } + }, + "my-other-dependency": { + "specVersion": "3.0", + "type": "does-not-exist", + "metadata": { + "name": "my-application" + } + } + }, + "dependencies": { + "my-dependency": [ + "my-other-dependency" + ], + "my-other-dependency": [ + "some-lib", + "some-other-lib" + ] + }, + "collections": { + "my-dependency": { + "modules": { + "lib-1": "src/lib1", + "lib-2": "src/lib2" + } + } + } + } + }); + await assertValidation(t, { + "specVersion": "2.0", + "kind": "extension", + "type": "project-shim", + "metadata": { + "name": "my-project-shim" + }, + "shims": { + "configurations": { + "invalid": { + "specVersion": "3.0", + "type": "does-not-exist", + "metadata": { + "name": "my-application" + } + } + }, + "dependencies": { + "my-dependency": { + "foo": "bar" + } + }, + "collections": { + "foo": { + "modules": { + "lib-1": { + "path": "src/lib1" + } + }, + "notAllowed": true + } + }, + "notAllowed": true + }, + "middleware": {} + }, [ + { + dataPath: "", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + "additionalProperty": "middleware" + }, + schemaPath: "#/additionalProperties", + }, + { + dataPath: "/shims", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + schemaPath: "#/properties/shims/additionalProperties", + }, + { + dataPath: "/shims/dependencies/my-dependency", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + schemaPath: "#/properties/shims/properties/dependencies/patternProperties/.%2B/type", + }, + { + dataPath: "/shims/collections/foo", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + schemaPath: "#/properties/shims/properties/collections/patternProperties/.%2B/additionalProperties" + }, + { + dataPath: "/shims/collections/foo/modules/lib-1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + schemaPath: "#/properties/shims/properties/collections/patternProperties/.%2B/properties/modules/patternProperties/.%2B/type" + } + ]); +}); diff --git a/test/lib/validation/schema/specVersion/2.0/kind/extension/server-middleware.js b/test/lib/validation/schema/specVersion/2.0/kind/extension/server-middleware.js new file mode 100644 index 000000000..fd14e2025 --- /dev/null +++ b/test/lib/validation/schema/specVersion/2.0/kind/extension/server-middleware.js @@ -0,0 +1,71 @@ +const test = require("ava"); +const AjvCoverage = require("../../../../../../../utils/AjvCoverage"); +const {_Validator: Validator} = require("../../../../../../../../lib/validation/validator"); +const ValidationError = require("../../../../../../../../lib/validation/ValidationError"); + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({config, project: {id: "my-project"}}); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError" + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator(); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/specVersion/2.0/kind/extension/server-middleware.json"] + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-extension-server-middleware"}); + const thresholds = { + statements: 60, + branches: 50, + functions: 100, + lines: 60 + }; + t.context.ajvCoverage.verify(thresholds); +}); + +test("kind: extension / type: server-middleware", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "extension", + "type": "server-middleware", + "metadata": { + "name": "my-server-middleware" + }, + "middleware": { + "path": "/foo" + } + }); + await assertValidation(t, { + "specVersion": "2.0", + "kind": "extension", + "type": "server-middleware", + "metadata": { + "name": "my-server-middleware" + }, + "middleware": { + "path": "/foo" + }, + "task": { + "path": "/bar" + } + }, [{ + dataPath: "", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + "additionalProperty": "task" + }, + schemaPath: "#/additionalProperties" + }]); +}); diff --git a/test/lib/validation/schema/specVersion/2.0/kind/extension/task.js b/test/lib/validation/schema/specVersion/2.0/kind/extension/task.js new file mode 100644 index 000000000..ceee57c1e --- /dev/null +++ b/test/lib/validation/schema/specVersion/2.0/kind/extension/task.js @@ -0,0 +1,69 @@ +const test = require("ava"); +const AjvCoverage = require("../../../../../../../utils/AjvCoverage"); +const {_Validator: Validator} = require("../../../../../../../../lib/validation/validator"); +const ValidationError = require("../../../../../../../../lib/validation/ValidationError"); + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({config, project: {id: "my-project"}}); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError" + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator(); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/specVersion/2.0/kind/extension/task.json"] + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-extension-task"}); + const thresholds = { + statements: 60, + branches: 50, + functions: 100, + lines: 60 + }; + t.context.ajvCoverage.verify(thresholds); +}); + +test("kind: extension / type: task", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "extension", + "type": "task", + "metadata": { + "name": "my-task" + }, + "task": { + "path": "/foo" + } + }); + await assertValidation(t, { + "specVersion": "2.0", + "kind": "extension", + "type": "task", + "metadata": { + "name": "my-task" + }, + "task": { + "path": "/foo" + }, + "resources": {} + }, [{ + dataPath: "", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + "additionalProperty": "resources" + }, + schemaPath: "#/additionalProperties" + }]); +}); diff --git a/test/lib/validation/schema/specVersion/2.0/kind/project.js b/test/lib/validation/schema/specVersion/2.0/kind/project.js new file mode 100644 index 000000000..69b83b20b --- /dev/null +++ b/test/lib/validation/schema/specVersion/2.0/kind/project.js @@ -0,0 +1,305 @@ +const test = require("ava"); +const AjvCoverage = require("../../../../../../utils/AjvCoverage"); +const {_Validator: Validator} = require("../../../../../../../lib/validation/validator"); +const ValidationError = require("../../../../../../../lib/validation/ValidationError"); + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({config, project: {id: "my-project"}}); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError" + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator(); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/specVersion/2.0/kind/project.json"] + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-project"}); + const thresholds = { + statements: 90, + branches: 80, + functions: 100, + lines: 90 + }; + t.context.ajvCoverage.verify(thresholds); +}); + +test("Type application", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "project", + "type": "application", + "metadata": { + "name": "my-application" + } + }); +}); + +test("Type application (no kind)", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "application", + "metadata": { + "name": "my-application" + } + }); +}); + +test("Type library", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "project", + "type": "library", + "metadata": { + "name": "my-library" + } + }); +}); + +test("Type library (no kind)", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "library", + "metadata": { + "name": "my-library" + } + }); +}); + +test("Type theme-library", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "project", + "type": "theme-library", + "metadata": { + "name": "my-theme-library" + } + }); +}); + +test("Type theme-library (no kind)", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "theme-library", + "metadata": { + "name": "my-theme-library" + } + }); +}); + +test("Type module", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "project", + "type": "module", + "metadata": { + "name": "my-module" + } + }); +}); + +test("Type module (no kind)", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "module", + "metadata": { + "name": "my-module" + } + }); +}); + +test("No type", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "project", + "metadata": { + "name": "my-project" + } + }, [{ + dataPath: "", + keyword: "required", + message: "should have required property 'type'", + params: { + missingProperty: "type", + }, + schemaPath: "#/required", + }]); +}); + +test("No type, no kind", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "metadata": { + "name": "my-project" + } + }, [{ + dataPath: "", + keyword: "required", + message: "should have required property 'type'", + params: { + missingProperty: "type", + }, + schemaPath: "#/required", + }]); +}); + +test("Invalid type", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "project", + "type": "foo", + "metadata": { + "name": "my-project" + } + }, [{ + dataPath: "/type", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "application", + "library", + "theme-library", + "module", + ], + }, + schemaPath: "#/properties/type/enum", + }]); +}); + +test("No specVersion", async (t) => { + await assertValidation(t, { + "kind": "project", + "type": "library", + "metadata": { + "name": "my-library" + } + }, [{ + dataPath: "", + keyword: "required", + message: "should have required property 'specVersion'", + params: { + missingProperty: "specVersion", + }, + schemaPath: "#/required", + }]); +}); + +test("No metadata", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "application" + }, [{ + dataPath: "", + keyword: "required", + message: "should have required property 'metadata'", + params: { + missingProperty: "metadata", + }, + schemaPath: "#/required", + }]); +}); + +test("Metadata not type object", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "application", + "metadata": "foo" + }, [{ + dataPath: "/metadata", + keyword: "type", + message: "should be object", + params: { + type: "object", + }, + schemaPath: "../ui5.json#/definitions/metadata/type", + }]); +}); + +test("No metadata.name", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "application", + "metadata": {} + }, [{ + dataPath: "/metadata", + keyword: "required", + message: "should have required property 'name'", + params: { + missingProperty: "name", + }, + schemaPath: "../ui5.json#/definitions/metadata/required", + }]); +}); + +test("Invalid metadata.name", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "application", + "metadata": { + "name": {} + } + }, [ + { + dataPath: "/metadata/name", + keyword: "type", + message: "should be string", + params: { + type: "string" + }, + schemaPath: "../ui5.json#/definitions/metadata/properties/name/type", + } + ]); +}); + +test("Invalid metadata.copyright", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "application", + "metadata": { + "name": "foo", + "copyright": 123 + } + }, [ + { + dataPath: "/metadata/copyright", + keyword: "type", + message: "should be string", + params: { + type: "string" + }, + schemaPath: "../ui5.json#/definitions/metadata/properties/copyright/type", + } + ]); +}); + +test("Additional metadata property", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "application", + "metadata": { + "name": "foo", + "copyrihgt": "typo" + } + }, [ + { + dataPath: "/metadata", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "copyrihgt" + }, + schemaPath: "../ui5.json#/definitions/metadata/additionalProperties", + } + ]); +}); diff --git a/test/lib/validation/schema/specVersion/2.0/kind/project/application.js b/test/lib/validation/schema/specVersion/2.0/kind/project/application.js new file mode 100644 index 000000000..29ea1a362 --- /dev/null +++ b/test/lib/validation/schema/specVersion/2.0/kind/project/application.js @@ -0,0 +1,632 @@ +const test = require("ava"); +const AjvCoverage = require("../../../../../../../utils/AjvCoverage"); +const {_Validator: Validator} = require("../../../../../../../../lib/validation/validator"); +const ValidationError = require("../../../../../../../../lib/validation/ValidationError"); + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({config, project: {id: "my-project"}}); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError" + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator(); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/specVersion/2.0/kind/project/application.json"] + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-project-application"}); + const thresholds = { + statements: 75, + branches: 65, + functions: 100, + lines: 75 + }; + t.context.ajvCoverage.verify(thresholds); +}); + +test("Valid configuration", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "project", + "type": "application", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "okay" + }, + "resources": { + "configuration": { + "propertiesFileSourceEncoding": "UTF-8", + "paths": { + "webapp": "my/path" + } + } + }, + "builder": { + "resources": { + "excludes": [ + "/resources/some/project/name/test_results/**", + "/test-resources/**", + "!/test-resources/some/project/name/demo-app/**" + ] + }, + "bundles": [ + { + "bundleDefinition": { + "name": "sap-ui-custom.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "mode": "raw", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true, + "resolveConditional": true, + "renderer": true, + "sort": true + }, + { + "mode": "provided", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": false, + "resolveConditional": false, + "renderer": false, + "sort": false + } + ] + }, + "bundleOptions": { + "optimize": true, + "decorateBootstrapModule": true, + "addTryCatchRestartWrapper": true, + "usePredefineCalls": true + } + }, + { + "bundleDefinition": { + "name": "app.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "mode": "preload", + "filters": [ + "some/app/Component.js" + ], + "resolve": true, + "sort": true + }, + { + "mode": "require", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true + } + ] + }, + "bundleOptions": { + "optimize": true, + "numberOfParts": 3 + } + } + ], + "componentPreload": { + "paths": [ + "some/glob/**/pattern/Component.js", + "some/other/glob/**/pattern/Component.js" + ], + "namespaces": [ + "some/namespace", + "some/other/namespace" + ] + }, + "cachebuster": { + "signatureType": "hash" + }, + "customTasks": [ + { + "name": "custom-task-1", + "beforeTask": "replaceCopyright", + "configuration": { + "some-key": "some value" + } + }, + { + "name": "custom-task-2", + "afterTask": "custom-task-1", + "configuration": { + "color": "blue" + } + }, + { + "name": "custom-task-2", + "beforeTask": "not-valid", + "configuration": false + } + ] + }, + "server": { + "settings": { + "httpPort": 1337, + "httpsPort": 1443 + }, + "customMiddleware": [ + { + "name": "myCustomMiddleware", + "mountPath": "/myapp", + "afterMiddleware": "compression", + "configuration": { + "debug": true + } + }, + { + "name": "myCustomMiddleware-2", + "beforeMiddleware": "myCustomMiddleware", + "configuration": { + "debug": true + } + } + ] + } + }); +}); + +test("Additional property", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "application", + "metadata": { + "name": "my-application" + }, + "notAllowed": true + }, [{ + dataPath: "", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + schemaPath: "#/additionalProperties", + }]); +}); + +test("Invalid resources configuration", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "application", + "metadata": { + "name": "com.sap.ui5.test" + }, + "resources": { + "configuration": { + "propertiesFileSourceEncoding": "FOO", + "paths": { + "app": "webapp", + "webapp": { + "path": "invalid" + } + }, + "notAllowed": true + }, + "notAllowed": true + } + }, [ + { + dataPath: "/resources", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + schemaPath: "#/additionalProperties", + }, + { + dataPath: "/resources/configuration", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + schemaPath: "#/properties/configuration/additionalProperties", + }, + { + dataPath: "/resources/configuration/propertiesFileSourceEncoding", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "UTF-8", + "ISO-8859-1" + ], + }, + schemaPath: "../project.json#/definitions/resources-configuration-propertiesFileSourceEncoding/enum" + }, + { + dataPath: "/resources/configuration/paths", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "app", + }, + schemaPath: "#/properties/configuration/properties/paths/additionalProperties", + }, + { + dataPath: "/resources/configuration/paths/webapp", + keyword: "type", + message: "should be string", + params: { + type: "string" + }, + schemaPath: "#/properties/configuration/properties/paths/properties/webapp/type" + } + ]); + await assertValidation(t, { + "specVersion": "2.0", + "type": "application", + "metadata": { + "name": "com.sap.ui5.test" + }, + "resources": { + "configuration": { + "paths": "webapp" + } + } + }, [ + { + dataPath: "/resources/configuration/paths", + keyword: "type", + message: "should be object", + params: { + type: "object" + }, + schemaPath: "#/properties/configuration/properties/paths/type", + } + ]); +}); + +test("Invalid builder configuration", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "application", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + // jsdoc is not supported for type application + "jsdoc": { + "excludes": [ + "some/project/name/thirdparty/**" + ] + }, + "bundles": [ + { + "bundleDefinition": { + "name": "sap-ui-custom.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "mode": "raw", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true, + "sort": true + } + ] + }, + "bundleOptions": { + "optimize": true + } + }, + { + "bundleDefinition": { + "defaultFileTypes": [ + ".js", true + ], + "sections": [ + { + "filters": [ + "some/app/Component.js" + ], + "resolve": true, + "sort": true + }, + { + "mode": "provide", + "filters": "*", + "resolve": true + } + ] + }, + "bundleOptions": { + "optimize": "true", + "numberOfParts": "3", + "notAllowed": true + } + } + ], + "componentPreload": { + "path": "some/invalid/path", + "paths": "some/invalid/glob/**/pattern/Component.js", + "namespaces": "some/invalid/namespace", + } + } + }, [ + { + dataPath: "/builder", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "jsdoc" + }, + schemaPath: "#/additionalProperties" + }, + { + dataPath: "/builder/bundles/1/bundleDefinition", + keyword: "required", + message: "should have required property 'name'", + params: { + missingProperty: "name", + }, + schemaPath: "../project.json#/definitions/builder-bundles/items/properties/bundleDefinition/required", + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/defaultFileTypes/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + schemaPath: "../project.json#/definitions/builder-bundles/items/properties/bundleDefinition/properties/defaultFileTypes/items/type", + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/0", + keyword: "required", + message: "should have required property 'mode'", + params: { + missingProperty: "mode", + }, + schemaPath: "../project.json#/definitions/builder-bundles/items/properties/bundleDefinition/properties/sections/items/required", + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/1/mode", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "raw", + "preload", + "require", + "provided", + ], + }, + schemaPath: "../project.json#/definitions/builder-bundles/items/properties/bundleDefinition/properties/sections/items/properties/mode/enum", + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/1/filters", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + schemaPath: "../project.json#/definitions/builder-bundles/items/properties/bundleDefinition/properties/sections/items/properties/filters/type", + }, + { + dataPath: "/builder/bundles/1/bundleOptions", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + schemaPath: "../project.json#/definitions/builder-bundles/items/properties/bundleOptions/additionalProperties", + }, + { + dataPath: "/builder/bundles/1/bundleOptions/optimize", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean", + }, + schemaPath: "../project.json#/definitions/builder-bundles/items/properties/bundleOptions/properties/optimize/type", + }, + { + dataPath: "/builder/bundles/1/bundleOptions/numberOfParts", + keyword: "type", + message: "should be number", + params: { + type: "number", + }, + schemaPath: "../project.json#/definitions/builder-bundles/items/properties/bundleOptions/properties/numberOfParts/type", + }, + { + dataPath: "/builder/componentPreload", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "path", + }, + schemaPath: "../project.json#/definitions/builder-componentPreload/additionalProperties", + }, + { + dataPath: "/builder/componentPreload/paths", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + schemaPath: "../project.json#/definitions/builder-componentPreload/properties/paths/type", + }, + { + dataPath: "/builder/componentPreload/namespaces", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + schemaPath: "../project.json#/definitions/builder-componentPreload/properties/namespaces/type", + } + ]); +}); + +test("framework configuration: OpenUI5", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "application", + "metadata": { + "name": "my-application" + }, + "framework": { + "name": "OpenUI5", + "version": "1.75.0", + "libraries": [ + {"name": "sap.ui.core"}, + {"name": "sap.m"}, + {"name": "sap.f", "optional": true}, + {"name": "sap.ui.support", "development": true} + ] + } + }); +}); + +test("framework configuration: SAPUI5", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "application", + "metadata": { + "name": "my-application" + }, + "framework": { + "name": "SAPUI5", + "version": "1.75.0", + "libraries": [ + {"name": "sap.ui.core"}, + {"name": "sap.m"}, + {"name": "sap.f", "optional": true}, + {"name": "sap.ui.support", "development": true} + ] + } + }); +}); + +test("framework configuration: Invalid", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "application", + "metadata": { + "name": "my-application" + }, + "framework": { + "name": "FooUI5", + "version": "1.75", + "libraries": [ + "sap.ui.core", + {"library": "sap.m"}, + {"name": "sap.f", "optional": "x"}, + {"name": "sap.f", "development": "no"} + ] + } + }, [ + { + dataPath: "/framework/name", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "OpenUI5", + "SAPUI5", + ], + }, + schemaPath: "../project.json#/definitions/framework/properties/name/enum", + }, + { + dataPath: "/framework/version", + keyword: "errorMessage", + message: "Not a valid version according to the Semantic Versioning specification (https://semver.org/)", + params: { + errors: [ + { + dataPath: "/framework/version", + keyword: "pattern", + message: + "should match pattern \"^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*" + + "|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+(" + + "[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$\"", + params: { + pattern: "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-]" + + "[0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(" + + "?:\\.[0-9a-zA-Z-]+)*))?$", + }, + schemaPath: "../project.json#/definitions/framework/properties/version/pattern", + } + ] + }, + schemaPath: "../project.json#/definitions/framework/properties/version/errorMessage", + }, + { + dataPath: "/framework/libraries/0", + keyword: "type", + message: "should be object", + params: { + type: "object", + }, + schemaPath: "../project.json#/definitions/framework/properties/libraries/items/type" + + }, + { + dataPath: "/framework/libraries/1", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "library", + }, + schemaPath: "../project.json#/definitions/framework/properties/libraries/items/additionalProperties", + }, + { + dataPath: "/framework/libraries/1", + keyword: "required", + message: "should have required property 'name'", + params: { + missingProperty: "name", + }, + schemaPath: "../project.json#/definitions/framework/properties/libraries/items/required", + }, + { + dataPath: "/framework/libraries/2/optional", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean" + }, + + schemaPath: "../project.json#/definitions/framework/properties/libraries/items/properties/optional/type", + }, + { + dataPath: "/framework/libraries/3/development", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean" + }, + schemaPath: "../project.json#/definitions/framework/properties/libraries/items/properties/development/type" + } + ]); +}); diff --git a/test/lib/validation/schema/specVersion/2.0/kind/project/library.js b/test/lib/validation/schema/specVersion/2.0/kind/project/library.js new file mode 100644 index 000000000..12c705964 --- /dev/null +++ b/test/lib/validation/schema/specVersion/2.0/kind/project/library.js @@ -0,0 +1,704 @@ +const test = require("ava"); +const AjvCoverage = require("../../../../../../../utils/AjvCoverage"); +const {_Validator: Validator} = require("../../../../../../../../lib/validation/validator"); +const ValidationError = require("../../../../../../../../lib/validation/ValidationError"); + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({config, project: {id: "my-project"}}); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError" + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator(); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/specVersion/2.0/kind/project/library.json"] + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-project-library"}); + const thresholds = { + statements: 75, + branches: 65, + functions: 100, + lines: 75 + }; + t.context.ajvCoverage.verify(thresholds); +}); + +test("Valid configuration", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "project", + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "resources": { + "configuration": { + "propertiesFileSourceEncoding": "UTF-8", + "paths": { + "src": "src/main/uilib", + "test": "src/test/uilib" + } + } + }, + "builder": { + "resources": { + "excludes": [ + "/resources/some/project/name/test_results/**", + "!/test-resources/some/project/name/demo-app/**" + ] + }, + "bundles": [ + { + "bundleDefinition": { + "name": "sap-ui-custom.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "mode": "raw", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true, + "resolveConditional": true, + "renderer": true, + "sort": true + }, + { + "mode": "provided", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": false, + "resolveConditional": false, + "renderer": false, + "sort": false + } + ] + }, + "bundleOptions": { + "optimize": true, + "decorateBootstrapModule": true, + "addTryCatchRestartWrapper": true, + "usePredefineCalls": true + } + }, + { + "bundleDefinition": { + "name": "app.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "mode": "preload", + "filters": [ + "some/app/Component.js" + ], + "resolve": true, + "sort": true + }, + { + "mode": "require", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true + } + ] + }, + "bundleOptions": { + "optimize": true, + "numberOfParts": 3 + } + } + ], + "componentPreload": { + "paths": [ + "some/glob/**/pattern/Component.js", + "some/other/glob/**/pattern/Component.js" + ], + "namespaces": [ + "some/namespace", + "some/other/namespace" + ] + }, + "jsdoc": { + "excludes": [ + "some/project/name/thirdparty/**" + ] + }, + "customTasks": [ + { + "name": "custom-task-1", + "beforeTask": "replaceCopyright", + "configuration": { + "some-key": "some value" + } + }, + { + "name": "custom-task-2", + "afterTask": "custom-task-1", + "configuration": { + "color": "blue" + } + } + ] + }, + "server": { + "settings": { + "httpPort": 1337, + "httpsPort": 1443 + }, + "customMiddleware": [ + { + "name": "myCustomMiddleware", + "mountPath": "/myapp", + "afterMiddleware": "compression", + "configuration": { + "debug": true + } + } + ] + } + }); +}); + +test("Invalid configuration", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "resources": { + "configuration": { + "propertiesFileSourceEncoding": "UTF8", + "paths": { + "src": {"path": "src"}, + "test": {"path": "test"}, + "webapp": "app" + } + } + }, + "builder": { + "resources": { + "excludes": "/resources/some/project/name/test_results/**" + }, + "bundles": [ + { + "bundleDefinition": { + "name": "sap-ui-custom.js", + "defaultFileTypes": [ + ".js" + ], + "sections": [ + { + "mode": "raw", + "filters": [ + "ui5loader-autoconfig.js" + ], + "resolve": true, + "sort": true + } + ] + }, + "bundleOptions": { + "optimize": true + } + }, + { + "bundleDefinition": { + "defaultFileTypes": [ + ".js", true + ], + "sections": [ + { + "filters": [ + "some/app/Component.js" + ], + "resolve": true, + "sort": true + }, + { + "mode": "provide", + "filters": "*", + "resolve": true + } + ] + }, + "bundleOptions": { + "optimize": "true", + "numberOfParts": "3", + "notAllowed": true + } + } + ], + "componentPreload": { + "path": "some/invalid/path", + "paths": "some/invalid/glob/**/pattern/Component.js", + "namespaces": "some/invalid/namespace", + }, + "jsdoc": { + "excludes": "some/project/name/thirdparty/**" + }, + "customTasks": [ + { + "name": "custom-task-1", + "beforeTask": "replaceCopyright", + "afterTask": "replaceCopyright", + }, + { + "afterTask": "custom-task-1", + "configuration": { + "color": "blue" + } + }, + "my-task" + ] + }, + "server": { + "settings": { + "httpPort": "1337", + "httpsPort": "1443" + } + } + }, [ + { + dataPath: "/resources/configuration/propertiesFileSourceEncoding", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "UTF-8", + "ISO-8859-1", + ], + }, + schemaPath: "../project.json#/definitions/resources-configuration-propertiesFileSourceEncoding/enum", + }, + { + dataPath: "/resources/configuration/paths", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "webapp", + }, + schemaPath: "#/properties/configuration/properties/paths/additionalProperties", + }, + { + dataPath: "/resources/configuration/paths/src", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + schemaPath: "#/properties/configuration/properties/paths/properties/src/type", + }, + { + dataPath: "/resources/configuration/paths/test", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + schemaPath: "#/properties/configuration/properties/paths/properties/test/type", + }, + { + dataPath: "/builder/resources/excludes", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + schemaPath: "../project.json#/definitions/builder-resources/properties/excludes/type", + }, + { + dataPath: "/builder/jsdoc/excludes", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + schemaPath: "#/properties/jsdoc/properties/excludes/type", + }, + { + dataPath: "/builder/bundles/1/bundleDefinition", + keyword: "required", + message: "should have required property 'name'", + params: { + missingProperty: "name", + }, + schemaPath: "../project.json#/definitions/builder-bundles/items/properties/bundleDefinition/required", + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/defaultFileTypes/1", + keyword: "type", + message: "should be string", + params: { + type: "string", + }, + schemaPath: "../project.json#/definitions/builder-bundles/items/properties/bundleDefinition/properties/defaultFileTypes/items/type", + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/0", + keyword: "required", + message: "should have required property 'mode'", + params: { + missingProperty: "mode", + }, + schemaPath: "../project.json#/definitions/builder-bundles/items/properties/bundleDefinition/properties/sections/items/required", + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/1/mode", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "raw", + "preload", + "require", + "provided", + ], + }, + schemaPath: "../project.json#/definitions/builder-bundles/items/properties/bundleDefinition/properties/sections/items/properties/mode/enum", + }, + { + dataPath: "/builder/bundles/1/bundleDefinition/sections/1/filters", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + schemaPath: "../project.json#/definitions/builder-bundles/items/properties/bundleDefinition/properties/sections/items/properties/filters/type", + }, + { + dataPath: "/builder/bundles/1/bundleOptions", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "notAllowed", + }, + schemaPath: "../project.json#/definitions/builder-bundles/items/properties/bundleOptions/additionalProperties", + }, + { + dataPath: "/builder/bundles/1/bundleOptions/optimize", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean", + }, + schemaPath: "../project.json#/definitions/builder-bundles/items/properties/bundleOptions/properties/optimize/type", + }, + { + dataPath: "/builder/bundles/1/bundleOptions/numberOfParts", + keyword: "type", + message: "should be number", + params: { + type: "number", + }, + schemaPath: "../project.json#/definitions/builder-bundles/items/properties/bundleOptions/properties/numberOfParts/type", + }, + { + dataPath: "/builder/componentPreload", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "path", + }, + schemaPath: "../project.json#/definitions/builder-componentPreload/additionalProperties", + }, + { + dataPath: "/builder/componentPreload/paths", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + schemaPath: "../project.json#/definitions/builder-componentPreload/properties/paths/type", + }, + { + dataPath: "/builder/componentPreload/namespaces", + keyword: "type", + message: "should be array", + params: { + type: "array", + }, + schemaPath: "../project.json#/definitions/builder-componentPreload/properties/namespaces/type", + }, + { + dataPath: "/builder/customTasks/0", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "afterTask", + }, + schemaPath: "../project.json#/definitions/customTasks/items/oneOf/0/additionalProperties", + }, + { + dataPath: "/builder/customTasks/0", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "beforeTask", + }, + schemaPath: "../project.json#/definitions/customTasks/items/oneOf/1/additionalProperties", + }, + { + dataPath: "/builder/customTasks/1", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "afterTask", + }, + schemaPath: "../project.json#/definitions/customTasks/items/oneOf/0/additionalProperties", + }, + { + dataPath: "/builder/customTasks/1", + keyword: "required", + message: "should have required property 'name'", + params: { + missingProperty: "name", + }, + schemaPath: "../project.json#/definitions/customTasks/items/oneOf/0/required", + }, + { + dataPath: "/builder/customTasks/1", + keyword: "required", + message: "should have required property 'beforeTask'", + params: { + missingProperty: "beforeTask", + }, + schemaPath: "../project.json#/definitions/customTasks/items/oneOf/0/required", + }, + { + dataPath: "/builder/customTasks/2", + keyword: "type", + message: "should be object", + params: { + type: "object", + }, + schemaPath: "../project.json#/definitions/customTasks/items/oneOf/0/type", + }, + { + dataPath: "/server/settings/httpPort", + keyword: "type", + message: "should be number", + params: { + type: "number", + }, + schemaPath: "../project.json#/definitions/server/properties/settings/properties/httpPort/type", + }, + { + dataPath: "/server/settings/httpsPort", + keyword: "type", + message: "should be number", + params: { + type: "number", + }, + schemaPath: "../project.json#/definitions/server/properties/settings/properties/httpsPort/type", + } + ]); +}); + +test("Additional property", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "foo": true + }, [{ + dataPath: "", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "foo" + }, + schemaPath: "#/additionalProperties" + }]); +}); + +test("Invalid builder configuration", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + // cachebuster is only supported for type application + "cachebuster": { + "signatureType": "time" + } + } + }, [{ + dataPath: "/builder", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "cachebuster" + }, + schemaPath: "#/additionalProperties" + }]); +}); + +test("framework configuration: OpenUI5", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "library", + "metadata": { + "name": "my-library" + }, + "framework": { + "name": "OpenUI5", + "version": "1.75.0", + "libraries": [ + {"name": "sap.ui.core"}, + {"name": "sap.m"}, + {"name": "sap.f", "optional": true}, + {"name": "sap.ui.support", "development": true} + ] + } + }); +}); + +test("framework configuration: SAPUI5", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "library", + "metadata": { + "name": "my-library" + }, + "framework": { + "name": "SAPUI5", + "version": "1.75.0", + "libraries": [ + {"name": "sap.ui.core"}, + {"name": "sap.m"}, + {"name": "sap.f", "optional": true}, + {"name": "sap.ui.support", "development": true} + ] + } + }); +}); + +test("framework configuration: Invalid", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "library", + "metadata": { + "name": "my-library" + }, + "framework": { + "name": "FooUI5", + "version": "1.75", + "libraries": [ + "sap.ui.core", + {"library": "sap.m"}, + {"name": "sap.f", "optional": "x"}, + {"name": "sap.f", "development": "no"} + ] + } + }, [ + { + dataPath: "/framework/name", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "OpenUI5", + "SAPUI5", + ], + }, + schemaPath: "../project.json#/definitions/framework/properties/name/enum", + }, + { + dataPath: "/framework/version", + keyword: "errorMessage", + message: "Not a valid version according to the Semantic Versioning specification (https://semver.org/)", + params: { + errors: [ + { + dataPath: "/framework/version", + keyword: "pattern", + message: + "should match pattern \"^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*" + + "|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+(" + + "[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$\"", + params: { + pattern: "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-]" + + "[0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(" + + "?:\\.[0-9a-zA-Z-]+)*))?$", + }, + schemaPath: "../project.json#/definitions/framework/properties/version/pattern", + } + ] + }, + schemaPath: "../project.json#/definitions/framework/properties/version/errorMessage", + }, + { + dataPath: "/framework/libraries/0", + keyword: "type", + message: "should be object", + params: { + type: "object", + }, + schemaPath: "../project.json#/definitions/framework/properties/libraries/items/type" + + }, + { + dataPath: "/framework/libraries/1", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "library", + }, + schemaPath: "../project.json#/definitions/framework/properties/libraries/items/additionalProperties", + }, + { + dataPath: "/framework/libraries/1", + keyword: "required", + message: "should have required property 'name'", + params: { + missingProperty: "name", + }, + schemaPath: "../project.json#/definitions/framework/properties/libraries/items/required", + }, + { + dataPath: "/framework/libraries/2/optional", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean" + }, + + schemaPath: "../project.json#/definitions/framework/properties/libraries/items/properties/optional/type", + }, + { + dataPath: "/framework/libraries/3/development", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean" + }, + schemaPath: "../project.json#/definitions/framework/properties/libraries/items/properties/development/type" + } + ]); +}); diff --git a/test/lib/validation/schema/specVersion/2.0/kind/project/module.js b/test/lib/validation/schema/specVersion/2.0/kind/project/module.js new file mode 100644 index 000000000..35c30ae34 --- /dev/null +++ b/test/lib/validation/schema/specVersion/2.0/kind/project/module.js @@ -0,0 +1,124 @@ +const test = require("ava"); +const AjvCoverage = require("../../../../../../../utils/AjvCoverage"); +const {_Validator: Validator} = require("../../../../../../../../lib/validation/validator"); +const ValidationError = require("../../../../../../../../lib/validation/ValidationError"); + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({config, project: {id: "my-project"}}); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError" + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator(); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/specVersion/2.0/kind/project/module.json"] + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-project-module"}); + const thresholds = { + statements: 65, + branches: 55, + functions: 100, + lines: 65 + }; + t.context.ajvCoverage.verify(thresholds); +}); + +test("Valid configuration", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "project", + "type": "module", + "metadata": { + "name": "my-module" + }, + "resources": { + "configuration": { + "paths": { + "/resources/my/library/module-xy/": "lib", + "/resources/my/library/module-xy-min/": "dist" + } + } + } + }); +}); + +test("No framework configuration", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "module", + "metadata": { + "name": "my-module" + }, + "framework": {} + }, [{ + dataPath: "", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + "additionalProperty": "framework" + }, + schemaPath: "#/additionalProperties" + }]); +}); + +test("No propertiesFileSourceEncoding configuration", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "module", + "metadata": { + "name": "my-module" + }, + "resources": { + "configuration": { + "propertiesFileSourceEncoding": "UTF-8" + } + } + }, [{ + dataPath: "/resources/configuration", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + "additionalProperty": "propertiesFileSourceEncoding" + }, + schemaPath: "#/properties/resources/properties/configuration/additionalProperties" + }]); +}); + +test("No builder, server configuration", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "module", + "metadata": { + "name": "my-module" + }, + "builder": {}, + "server": {} + }, [{ + dataPath: "", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + "additionalProperty": "builder" + }, + schemaPath: "#/additionalProperties" + }, { + dataPath: "", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + "additionalProperty": "server" + }, + schemaPath: "#/additionalProperties" + }]); +}); diff --git a/test/lib/validation/schema/specVersion/2.0/kind/project/theme-library.js b/test/lib/validation/schema/specVersion/2.0/kind/project/theme-library.js new file mode 100644 index 000000000..2ca97d50b --- /dev/null +++ b/test/lib/validation/schema/specVersion/2.0/kind/project/theme-library.js @@ -0,0 +1,299 @@ +const test = require("ava"); +const AjvCoverage = require("../../../../../../../utils/AjvCoverage"); +const {_Validator: Validator} = require("../../../../../../../../lib/validation/validator"); +const ValidationError = require("../../../../../../../../lib/validation/ValidationError"); + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({config, project: {id: "my-project"}}); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError" + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator(); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/specVersion/2.0/kind/project/theme-library.json"] + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-project-theme-library"}); + const thresholds = { + statements: 70, + branches: 60, + functions: 100, + lines: 70 + }; + t.context.ajvCoverage.verify(thresholds); +}); + +test("Valid configuration", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "theme-library", + "metadata": { + "name": "my-theme-library", + "copyright": "Copyright goes here" + }, + "resources": { + "configuration": { + "propertiesFileSourceEncoding": "UTF-8", + "paths": { + "src": "src/main/uilib", + "test": "src/test/uilib" + } + } + }, + "builder": { + "resources": { + "excludes": [ + "/resources/some/project/name/test_results/**", + "/test-resources/**", + "!/test-resources/some/project/name/demo-app/**" + ] + }, + "customTasks": [ + { + "name": "custom-task-1", + "beforeTask": "replaceCopyright", + "configuration": { + "some-key": "some value" + } + }, + { + "name": "custom-task-2", + "afterTask": "custom-task-1", + "configuration": { + "color": "blue" + } + } + ] + }, + "server": { + "settings": { + "httpPort": 1337, + "httpsPort": 1443 + }, + "customMiddleware": [ + { + "name": "myCustomMiddleware", + "mountPath": "/myapp", + "afterMiddleware": "compression", + "configuration": { + "debug": true + } + } + ] + } + }); +}); + +test("Additional property", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "theme-library", + "metadata": { + "name": "my-theme-library" + }, + "foo": true + }, [{ + dataPath: "", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + "additionalProperty": "foo" + }, + schemaPath: "#/additionalProperties" + }]); +}); + +test("Invalid builder configuration", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "theme-library", + "metadata": { + "name": "com.sap.ui5.test", + "copyright": "yes" + }, + "builder": { + // cachebuster is only supported for type application + "cachebuster": { + "signatureType": "time" + }, + // jsdoc is only supported for type library + "jsdoc": { + "excludes": [ + "some/project/name/thirdparty/**" + ] + } + } + }, [{ + dataPath: "/builder", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "cachebuster" + }, + schemaPath: "#/additionalProperties" + }, + { + dataPath: "/builder", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "jsdoc" + }, + schemaPath: "#/additionalProperties" + }]); +}); + +test("framework configuration: OpenUI5", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "theme-library", + "metadata": { + "name": "my-theme-library" + }, + "framework": { + "name": "OpenUI5", + "version": "1.75.0", + "libraries": [ + {"name": "sap.ui.core"}, + {"name": "sap.m"}, + {"name": "sap.f", "optional": true}, + {"name": "sap.ui.support", "development": true} + ] + } + }); +}); + +test("framework configuration: SAPUI5", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "theme-library", + "metadata": { + "name": "my-theme-library" + }, + "framework": { + "name": "SAPUI5", + "version": "1.75.0", + "libraries": [ + {"name": "sap.ui.core"}, + {"name": "sap.m"}, + {"name": "sap.f", "optional": true}, + {"name": "sap.ui.support", "development": true} + ] + } + }); +}); + +test("framework configuration: Invalid", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "theme-library", + "metadata": { + "name": "my-theme-library" + }, + "framework": { + "name": "FooUI5", + "version": "1.75", + "libraries": [ + "sap.ui.core", + {"library": "sap.m"}, + {"name": "sap.f", "optional": "x"}, + {"name": "sap.f", "development": "no"} + ] + } + }, [ + { + dataPath: "/framework/name", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "OpenUI5", + "SAPUI5", + ], + }, + schemaPath: "../project.json#/definitions/framework/properties/name/enum", + }, + { + dataPath: "/framework/version", + keyword: "errorMessage", + message: "Not a valid version according to the Semantic Versioning specification (https://semver.org/)", + params: { + errors: [ + { + dataPath: "/framework/version", + keyword: "pattern", + message: + "should match pattern \"^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*" + + "|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+(" + + "[0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$\"", + params: { + pattern: "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-]" + + "[0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(" + + "?:\\.[0-9a-zA-Z-]+)*))?$", + }, + schemaPath: "../project.json#/definitions/framework/properties/version/pattern", + } + ] + }, + schemaPath: "../project.json#/definitions/framework/properties/version/errorMessage", + }, + { + dataPath: "/framework/libraries/0", + keyword: "type", + message: "should be object", + params: { + type: "object", + }, + schemaPath: "../project.json#/definitions/framework/properties/libraries/items/type" + + }, + { + dataPath: "/framework/libraries/1", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "library", + }, + schemaPath: "../project.json#/definitions/framework/properties/libraries/items/additionalProperties", + }, + { + dataPath: "/framework/libraries/1", + keyword: "required", + message: "should have required property 'name'", + params: { + missingProperty: "name", + }, + schemaPath: "../project.json#/definitions/framework/properties/libraries/items/required", + }, + { + dataPath: "/framework/libraries/2/optional", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean" + }, + + schemaPath: "../project.json#/definitions/framework/properties/libraries/items/properties/optional/type", + }, + { + dataPath: "/framework/libraries/3/development", + keyword: "type", + message: "should be boolean", + params: { + type: "boolean" + }, + schemaPath: "../project.json#/definitions/framework/properties/libraries/items/properties/development/type" + } + ]); +}); diff --git a/test/lib/validation/schema/ui5.js b/test/lib/validation/schema/ui5.js new file mode 100644 index 000000000..f90da6fa8 --- /dev/null +++ b/test/lib/validation/schema/ui5.js @@ -0,0 +1,269 @@ +const test = require("ava"); +const AjvCoverage = require("../../../utils/AjvCoverage"); +const {_Validator: Validator} = require("../../../../lib/validation/validator"); +const ValidationError = require("../../../../lib/validation/ValidationError"); + +async function assertValidation(t, config, expectedErrors = undefined) { + const validation = t.context.validator.validate({config, project: {id: "my-project"}}); + if (expectedErrors) { + const validationError = await t.throwsAsync(validation, { + instanceOf: ValidationError, + name: "ValidationError" + }); + t.deepEqual(validationError.errors, expectedErrors); + } else { + await t.notThrowsAsync(validation); + } +} + +test.before((t) => { + t.context.validator = new Validator(); + t.context.ajvCoverage = new AjvCoverage(t.context.validator.ajv, { + includes: ["schema/ui5.json"] + }); +}); + +test.after.always((t) => { + t.context.ajvCoverage.createReport("html", {dir: "coverage/ajv-ui5"}); + const thresholds = { + statements: 95, + branches: 80, + functions: 100, + lines: 95 + }; + t.context.ajvCoverage.verify(thresholds); +}); + +test("Undefined", async (t) => { + await assertValidation(t, undefined, [{ + dataPath: "", + keyword: "type", + message: "should be object", + params: { + type: "object", + }, + schemaPath: "#/type", + }]); +}); + +test("Missing specVersion, type, metadata", async (t) => { + await assertValidation(t, {}, [ + { + dataPath: "", + keyword: "required", + message: "should have required property 'specVersion'", + params: { + missingProperty: "specVersion", + }, + schemaPath: "#/required", + }, + { + dataPath: "", + keyword: "required", + message: "should have required property 'metadata'", + params: { + missingProperty: "metadata", + }, + schemaPath: "#/required", + }, + { + dataPath: "", + keyword: "required", + message: "should have required property 'type'", + params: { + missingProperty: "type", + }, + schemaPath: "#/required", + } + + ]); +}); + +test("Missing type, metadata", async (t) => { + await assertValidation(t, { + "specVersion": "2.0" + }, [ + { + dataPath: "", + keyword: "required", + message: "should have required property 'metadata'", + params: { + missingProperty: "metadata", + }, + schemaPath: "#/required", + }, + { + dataPath: "", + keyword: "required", + message: "should have required property 'type'", + params: { + missingProperty: "type", + }, + schemaPath: "#/required", + } + ]); +}); + +test("Invalid specVersion", async (t) => { + await assertValidation(t, { + "specVersion": "0.0" + }, [ + { + dataPath: "/specVersion", + keyword: "errorMessage", + message: +`Unsupported "specVersion" +Your UI5 CLI installation might be outdated. +Supported specification versions: "2.0", "1.1", "1.0", "0.1" +For details see: https://sap.github.io/ui5-tooling/pages/Configuration/#specification-versions`, + params: { + errors: [ + { + dataPath: "/specVersion", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "2.0", + "1.1", + "1.0", + "0.1", + ], + }, + schemaPath: "#/properties/specVersion/enum", + }, + ], + }, + schemaPath: "#/properties/specVersion/errorMessage", + } + ]); +}); + +test("Invalid type", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "foo", + "metadata": { + "name": "foo" + } + }, [ + { + dataPath: "/type", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "application", + "library", + "theme-library", + "module" + ] + }, + schemaPath: "#/properties/type/enum", + } + ]); +}); + +test("Invalid kind", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "kind": "foo", + "metadata": { + "name": "foo" + } + }, [ + { + dataPath: "/kind", + keyword: "enum", + message: "should be equal to one of the allowed values", + params: { + allowedValues: [ + "project", + "extension", + null + ], + }, + schemaPath: "#/properties/kind/enum", + } + ]); +}); + +test("Invalid metadata.name", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "application", + "metadata": { + "name": {} + } + }, [ + { + dataPath: "/metadata/name", + keyword: "type", + message: "should be string", + params: { + type: "string" + }, + schemaPath: "../ui5.json#/definitions/metadata/properties/name/type", + } + ]); +}); + +test("Invalid metadata.copyright", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "application", + "metadata": { + "name": "foo", + "copyright": 123 + } + }, [ + { + dataPath: "/metadata/copyright", + keyword: "type", + message: "should be string", + params: { + type: "string" + }, + schemaPath: "../ui5.json#/definitions/metadata/properties/copyright/type", + } + ]); +}); + +test("Additional metadata property", async (t) => { + await assertValidation(t, { + "specVersion": "2.0", + "type": "application", + "metadata": { + "name": "foo", + "copyrihgt": "typo" + } + }, [ + { + dataPath: "/metadata", + keyword: "additionalProperties", + message: "should NOT have additional properties", + params: { + additionalProperty: "copyrihgt" + }, + schemaPath: "../ui5.json#/definitions/metadata/additionalProperties", + } + ]); +}); + +test("specVersion 0.1", async (t) => { + await assertValidation(t, { + "specVersion": "0.1" + }); +}); + +test("specVersion 1.0", async (t) => { + await assertValidation(t, { + "specVersion": "1.0" + }); +}); + +test("specVersion 1.1", async (t) => { + await assertValidation(t, { + "specVersion": "1.1" + }); +}); diff --git a/test/lib/validation/validator.js b/test/lib/validation/validator.js new file mode 100644 index 000000000..b9bd68636 --- /dev/null +++ b/test/lib/validation/validator.js @@ -0,0 +1,22 @@ +const test = require("ava"); +const sinon = require("sinon"); +const {validate, _Validator: Validator} = require("../../../lib/validation/validator"); + +test.afterEach.always((t) => { + sinon.restore(); +}); + +test.serial("validate function calls Validator#validate method", async (t) => { + const config = {config: true}; + const project = {project: true}; + const yaml = {yaml: true}; + + const validateStub = sinon.stub(Validator.prototype, "validate"); + validateStub.resolves(); + + const result = await validate({config, project, yaml}); + + t.is(result, undefined, "validate should return undefined"); + t.is(validateStub.callCount, 1, "validate should be called once"); + t.deepEqual(validateStub.getCall(0).args, [{config, project, yaml}]); +}); diff --git a/test/utils/AjvCoverage.js b/test/utils/AjvCoverage.js new file mode 100644 index 000000000..e674c8d67 --- /dev/null +++ b/test/utils/AjvCoverage.js @@ -0,0 +1,146 @@ +// Inspired by https://github.com/epoberezkin/ajv-istanbul + +const crypto = require("crypto"); +const beautify = require("js-beautify").js_beautify; +const libReport = require("istanbul-lib-report"); +const reports = require("istanbul-reports"); +const libCoverage = require("istanbul-lib-coverage"); +const {createInstrumenter} = require("istanbul-lib-instrument"); + +const rSchemaName = new RegExp(/sourceURL=([^\s]*)/); + +const rRootDataUndefined = /\n(?:\s)*if \(rootData === undefined\) rootData = data;/g; +const rEnsureErrorArray = /\n(?:\s)*if \(vErrors === null\) vErrors = \[err\];(?:\s)*else vErrors\.push\(err\);/g; +const rDataPathOrEmptyString = /dataPath: \(dataPath \|\| ''\)/g; + +function hash(content) { + return crypto.createHash("sha1").update(content).digest("hex").substr(0, 16); +} + +function randomCoverageVar() { + return "__ajv-coverage__" + hash((String(Date.now()) + Math.random())); +} + +class AjvCoverage { + constructor(ajv, options = {}) { + this.ajv = ajv; + this.ajv._opts.processCode = this._processCode.bind(this); + if (options.meta === true) { + this.ajv._metaOpts.processCode = this._processCode.bind(this); + } + this._processFileName = options.processFileName; + this._includes = options.includes; + this._sources = {}; + this._globalCoverageVar = options.globalCoverage === true ? "__coverage__" : randomCoverageVar(); + this._instrumenter = createInstrumenter({ + coverageVariable: this._globalCoverageVar + }); + } + getSummary() { + const coverageMap = this._createCoverageMap(); + const summary = libCoverage.createCoverageSummary(); + + const files = coverageMap.files(); + files.forEach(function(file) { + const fileCoverageSummary = coverageMap.fileCoverageFor(file).toSummary(); + summary.merge(fileCoverageSummary); + return; + }); + + if (files.length === 0 || summary.lines.covered === 0) { + throw new Error("AjvCoverage#getSummary: No coverage data found!"); + } + + return { + branches: summary.branches.pct, + lines: summary.lines.pct, + statements: summary.statements.pct, + functions: summary.functions.pct + }; + } + verify(thresholds) { + const thresholdEntries = Object.entries(thresholds); + if (thresholdEntries.length === 0) { + throw new Error("AjvCoverage#verify: No thresholds defined!"); + } + + const summary = this.getSummary(); + const errors = []; + + thresholdEntries.forEach(function([threshold, expectedPct]) { + const pct = summary[threshold]; + if (pct === undefined) { + errors.push(`Invalid coverage threshold '${threshold}'`); + } else if (pct < expectedPct) { + errors.push( + `Coverage for '${threshold}' (${pct}%) ` + + `does not meet global threshold (${expectedPct}%)`); + } + }); + + if (errors.length > 0) { + const errorMessage = "ERROR:\n" + errors.join("\n"); + throw new Error(errorMessage); + } + } + createReport(name, contextOptions = {}, reportOptions = {}) { + const coverageMap = this._createCoverageMap(); + const context = libReport.createContext(Object.assign({}, contextOptions, { + coverageMap, + sourceFinder: (filePath) => { + if (this._sources[filePath]) { + return this._sources[filePath]; + } + const sourceFinder = contextOptions.sourceFinder; + if (typeof sourceFinder === "function") { + return sourceFinder(filePath); + } + } + })); + const report = reports.create(name, reportOptions); + report.execute(context); + } + _createCoverageMap() { + return libCoverage.createCoverageMap(global[this._globalCoverageVar]); + } + _processCode(originalCode) { + let fileName; + const schemaNameMatch = rSchemaName.exec(originalCode); + if (schemaNameMatch) { + fileName = schemaNameMatch[1]; + } else { + // Probably a definition of a schema that is compiled separately + // Try to find the schema that is currently compiling + const schemas = Object.entries(this.ajv._schemas); + const compilingSchemas = schemas.filter(([, schema]) => schema.compiling); + if (compilingSchemas.length > 0) { + // Last schema is the current one + const lastSchemaEntry = compilingSchemas[compilingSchemas.length - 1]; + fileName = lastSchemaEntry[0] + "-" + hash(originalCode); + } else { + fileName = hash(originalCode); + } + } + + if (typeof this._processFileName === "function") { + fileName = this._processFileName.call(null, fileName); + } + + if (this._includes && this._includes.every((pattern) => !fileName.includes(pattern))) { + return originalCode; + } + + const code = AjvCoverage.insertIgnoreComments(beautify(originalCode, {indent_size: 2})); + const instrumentedCode = this._instrumenter.instrumentSync(code, fileName); + this._sources[fileName] = code; + return instrumentedCode; + } + static insertIgnoreComments(code) { + code = code.replace(rRootDataUndefined, "\n/* istanbul ignore next */$&"); + code = code.replace(rEnsureErrorArray, "\n/* istanbul ignore next */$&"); + code = code.replace(rDataPathOrEmptyString, "dataPath: (dataPath || /* istanbul ignore next */ '')"); + return code; + } +} + +module.exports = AjvCoverage;