project/lib/validation/validator.js

  1. import {fileURLToPath} from "node:url";
  2. import {readFile} from "node:fs/promises";
  3. /**
  4. * @module @ui5/project/validation/validator
  5. * @description A collection of validation related APIs
  6. * @public
  7. */
  8. /**
  9. * @enum {string}
  10. * @private
  11. * @readonly
  12. */
  13. export const SCHEMA_VARIANTS = {
  14. "ui5": "ui5.json",
  15. "ui5-workspace": "ui5-workspace.json"
  16. };
  17. class Validator {
  18. constructor({Ajv, ajvErrors, schemaName, ajvConfig}) {
  19. if (!schemaName || !SCHEMA_VARIANTS[schemaName]) {
  20. throw new Error(
  21. `"schemaName" is missing or incorrect. The available schemaName variants are ${Object.keys(
  22. SCHEMA_VARIANTS
  23. ).join(", ")}`
  24. );
  25. }
  26. this._schemaName = SCHEMA_VARIANTS[schemaName];
  27. ajvConfig = Object.assign({
  28. allErrors: true,
  29. jsonPointers: true,
  30. loadSchema: Validator.loadSchema
  31. }, ajvConfig);
  32. this.ajv = new Ajv(ajvConfig);
  33. ajvErrors(this.ajv);
  34. }
  35. _compileSchema() {
  36. const schemaName = this._schemaName;
  37. if (!this._compiling) {
  38. this._compiling = Promise.resolve().then(async () => {
  39. const schema = await Validator.loadSchema(schemaName);
  40. const validate = await this.ajv.compileAsync(schema);
  41. return validate;
  42. });
  43. }
  44. return this._compiling;
  45. }
  46. async validate({config, project, yaml}) {
  47. const fnValidate = await this._compileSchema();
  48. const valid = fnValidate(config);
  49. if (!valid) {
  50. // Read errors/schema from fnValidate before lazy loading ValidationError module.
  51. // Otherwise they might be cleared already.
  52. const {errors, schema} = fnValidate;
  53. const {default: ValidationError} = await import("./ValidationError.js");
  54. throw new ValidationError({
  55. errors,
  56. schema,
  57. project,
  58. yaml
  59. });
  60. }
  61. }
  62. static async loadSchema(schemaPath) {
  63. const filePath = schemaPath.replace("http://ui5.sap/schema/", "");
  64. const schemaFile = await readFile(
  65. fileURLToPath(new URL(`./schema/${filePath}`, import.meta.url)), {encoding: "utf8"}
  66. );
  67. return JSON.parse(schemaFile);
  68. }
  69. }
  70. const validator = Object.create(null);
  71. const defaultsValidator = Object.create(null);
  72. async function _validate(schemaName, options) {
  73. if (!validator[schemaName]) {
  74. validator[schemaName] = (async () => {
  75. const {default: Ajv} = await import("ajv");
  76. const {default: ajvErrors} = await import("ajv-errors");
  77. return new Validator({Ajv, ajvErrors, schemaName});
  78. })();
  79. }
  80. const schemaValidator = await validator[schemaName];
  81. await schemaValidator.validate(options);
  82. }
  83. async function _validateAndSetDefaults(schemaName, options) {
  84. if (!defaultsValidator[schemaName]) {
  85. defaultsValidator[schemaName] = (async () => {
  86. const {default: Ajv} = await import("ajv");
  87. const {default: ajvErrors} = await import("ajv-errors");
  88. return new Validator({Ajv, ajvErrors, ajvConfig: {useDefaults: true}, schemaName});
  89. })();
  90. }
  91. // When AJV is configured with useDefaults: true, it may add properties to the
  92. // provided configuration that were not initially present. This behavior can
  93. // lead to unexpected side effects and potential issues. To avoid these
  94. // problems, we create a copy of the configuration. If we need the altered
  95. // configuration later, we return this copied version.
  96. const optionsCopy = structuredClone(options);
  97. const schemaValidator = await defaultsValidator[schemaName];
  98. await schemaValidator.validate(optionsCopy);
  99. return optionsCopy;
  100. }
  101. /**
  102. * Validates the given ui5 configuration.
  103. *
  104. * @public
  105. * @function
  106. * @static
  107. * @param {object} options
  108. * @param {object} options.config UI5 Configuration to validate
  109. * @param {object} options.project Project information
  110. * @param {string} options.project.id ID of the project
  111. * @param {object} [options.yaml] YAML information
  112. * @param {string} options.yaml.path Path of the YAML file
  113. * @param {string} options.yaml.source Content of the YAML file
  114. * @param {number} [options.yaml.documentIndex=0] Document index in case the YAML file contains multiple documents
  115. * @throws {@ui5/project/validation/ValidationError}
  116. * Rejects with a {@link @ui5/project/validation/ValidationError ValidationError}
  117. * when the validation fails.
  118. * @returns {Promise<undefined>} Returns a Promise that resolves when the validation succeeds
  119. */
  120. export async function validate(options) {
  121. await _validate("ui5", options);
  122. }
  123. /**
  124. * Validates the given ui5 configuration and returns default values if none are provided.
  125. *
  126. * @public
  127. * @function
  128. * @static
  129. * @param {object} options
  130. * @param {object} options.config The UI5 Configuration to validate
  131. * @param {object} options.project Project information
  132. * @param {string} options.project.id ID of the project
  133. * @param {object} [options.yaml] YAML information
  134. * @param {string} options.yaml.path Path of the YAML file
  135. * @param {string} options.yaml.source Content of the YAML file
  136. * @param {number} [options.yaml.documentIndex=0] Document index in case the YAML file contains multiple documents
  137. * @throws {module:@ui5/project/validation/ValidationError}
  138. * Rejects with a {@link @ui5/project/validation/ValidationError ValidationError}
  139. * when the validation fails.
  140. * @returns {Promise<options>} Returns a Promise that resolves when the validation succeeds
  141. */
  142. export async function getDefaults(options) {
  143. return await _validateAndSetDefaults("ui5", options);
  144. }
  145. /**
  146. * Enhances bundleDefinition by adding missing properties with their respective default values.
  147. *
  148. * @param {object[]} bundles Bundles to be enhanced
  149. * @param {module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundleDefinition} bundles[].bundleDefinition
  150. * Module bundle definition
  151. * @param {module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundleOptions} [bundles[].bundleOptions]
  152. * Module bundle options
  153. * @param {module:@ui5/project/specifications/Project} project The project to get metadata from
  154. * @returns {Promise<object>} The enhanced BundleDefinition & BundleOptions
  155. */
  156. export async function enhanceBundlesWithDefaults(bundles, project) {
  157. const config = {
  158. specVersion: `${project.getSpecVersion()}`,
  159. type: `${project.getType()}`,
  160. metadata: {name: project.getName()},
  161. builder: {bundles}
  162. };
  163. const result = await getDefaults({config, project: {id: project.getName()}});
  164. return result.config.builder.bundles;
  165. }
  166. /**
  167. * Validates the given ui5-workspace configuration.
  168. *
  169. * @public
  170. * @function
  171. * @static
  172. * @param {object} options
  173. * @param {object} options.config ui5-workspace Configuration to validate
  174. * @param {object} [options.yaml] YAML information
  175. * @param {string} options.yaml.path Path of the YAML file
  176. * @param {string} options.yaml.source Content of the YAML file
  177. * @param {number} [options.yaml.documentIndex=0] Document index in case the YAML file contains multiple documents
  178. * @throws {@ui5/project/validation/ValidationError}
  179. * Rejects with a {@link @ui5/project/validation/ValidationError ValidationError}
  180. * when the validation fails.
  181. * @returns {Promise<undefined>} Returns a Promise that resolves when the validation succeeds
  182. */
  183. export async function validateWorkspace(options) {
  184. await _validate("ui5-workspace", options);
  185. }
  186. export {
  187. /**
  188. * For testing only!
  189. *
  190. * @private
  191. */
  192. Validator as _Validator
  193. };