UNPKG

@ui5/project

Version:
215 lines (193 loc) 7.17 kB
import {fileURLToPath} from "node:url"; import {readFile} from "node:fs/promises"; /** * @module @ui5/project/validation/validator * @description A collection of validation related APIs * @public */ /** * @enum {string} * @private * @readonly */ export const SCHEMA_VARIANTS = { "ui5": "ui5.json", "ui5-workspace": "ui5-workspace.json" }; class Validator { constructor({Ajv, ajvErrors, schemaName, ajvConfig}) { if (!schemaName || !SCHEMA_VARIANTS[schemaName]) { throw new Error( `"schemaName" is missing or incorrect. The available schemaName variants are ${Object.keys( SCHEMA_VARIANTS ).join(", ")}` ); } this._schemaName = SCHEMA_VARIANTS[schemaName]; ajvConfig = Object.assign({ allErrors: true, jsonPointers: true, loadSchema: Validator.loadSchema }, ajvConfig); this.ajv = new Ajv(ajvConfig); ajvErrors(this.ajv); } _compileSchema() { const schemaName = this._schemaName; if (!this._compiling) { this._compiling = Promise.resolve().then(async () => { const schema = await Validator.loadSchema(schemaName); 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) { // Read errors/schema from fnValidate before lazy loading ValidationError module. // Otherwise they might be cleared already. const {errors, schema} = fnValidate; const {default: ValidationError} = await import("./ValidationError.js"); throw new ValidationError({ errors, schema, project, yaml }); } } static async loadSchema(schemaPath) { const filePath = schemaPath.replace("http://ui5.sap/schema/", ""); const schemaFile = await readFile( fileURLToPath(new URL(`./schema/${filePath}`, import.meta.url)), {encoding: "utf8"} ); return JSON.parse(schemaFile); } } const validator = Object.create(null); const defaultsValidator = Object.create(null); async function _validate(schemaName, options) { if (!validator[schemaName]) { validator[schemaName] = (async () => { const {default: Ajv} = await import("ajv"); const {default: ajvErrors} = await import("ajv-errors"); return new Validator({Ajv, ajvErrors, schemaName}); })(); } const schemaValidator = await validator[schemaName]; await schemaValidator.validate(options); } async function _validateAndSetDefaults(schemaName, options) { if (!defaultsValidator[schemaName]) { defaultsValidator[schemaName] = (async () => { const {default: Ajv} = await import("ajv"); const {default: ajvErrors} = await import("ajv-errors"); return new Validator({Ajv, ajvErrors, ajvConfig: {useDefaults: true}, schemaName}); })(); } // When AJV is configured with useDefaults: true, it may add properties to the // provided configuration that were not initially present. This behavior can // lead to unexpected side effects and potential issues. To avoid these // problems, we create a copy of the configuration. If we need the altered // configuration later, we return this copied version. const optionsCopy = structuredClone(options); const schemaValidator = await defaultsValidator[schemaName]; await schemaValidator.validate(optionsCopy); return optionsCopy; } /** * Validates the given ui5 configuration. * * @public * @function * @static * @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 {@ui5/project/validation/ValidationError} * Rejects with a {@link @ui5/project/validation/ValidationError ValidationError} * when the validation fails. * @returns {Promise<undefined>} Returns a Promise that resolves when the validation succeeds */ export async function validate(options) { await _validate("ui5", options); } /** * Validates the given ui5 configuration and returns default values if none are provided. * * @public * @function * @static * @param {object} options * @param {object} options.config The 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 @ui5/project/validation/ValidationError ValidationError} * when the validation fails. * @returns {Promise<options>} Returns a Promise that resolves when the validation succeeds */ export async function getDefaults(options) { return await _validateAndSetDefaults("ui5", options); } /** * Enhances bundleDefinition by adding missing properties with their respective default values. * * @param {object[]} bundles Bundles to be enhanced * @param {module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundleDefinition} bundles[].bundleDefinition * Module bundle definition * @param {module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundleOptions} [bundles[].bundleOptions] * Module bundle options * @param {module:@ui5/project/specifications/Project} project The project to get metadata from * @returns {Promise<object>} The enhanced BundleDefinition & BundleOptions */ export async function enhanceBundlesWithDefaults(bundles, project) { const config = { specVersion: `${project.getSpecVersion()}`, type: `${project.getType()}`, metadata: {name: project.getName()}, builder: {bundles} }; const result = await getDefaults({config, project: {id: project.getName()}}); return result.config.builder.bundles; } /** * Validates the given ui5-workspace configuration. * * @public * @function * @static * @param {object} options * @param {object} options.config ui5-workspace Configuration to validate * @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 {@ui5/project/validation/ValidationError} * Rejects with a {@link @ui5/project/validation/ValidationError ValidationError} * when the validation fails. * @returns {Promise<undefined>} Returns a Promise that resolves when the validation succeeds */ export async function validateWorkspace(options) { await _validate("ui5-workspace", options); } export { /** * For testing only! * * @private */ Validator as _Validator };