diff --git a/lib/build/definitions/application.js b/lib/build/definitions/application.js index cb55a2a06..6b346ca9f 100644 --- a/lib/build/definitions/application.js +++ b/lib/build/definitions/application.js @@ -1,4 +1,5 @@ import {enhancePatternWithExcludes} from "./_utils.js"; +import {enhanceBundlesWithDefaults} from "../../validation/validator.js"; /** * Get tasks and their configuration for a given application project @@ -84,7 +85,10 @@ export default function({project, taskUtil, getTask}) { requiresDependencies: true, taskFunction: async ({workspace, dependencies, taskUtil, options}) => { const generateBundleTask = await getTask("generateBundle"); - return bundles.reduce(function(sequence, bundle) { + // Async resolve default values for bundle definitions and options + const bundlesDefaults = await enhanceBundlesWithDefaults(bundles, taskUtil.getProject()); + + return bundlesDefaults.reduce(async function(sequence, bundle) { return sequence.then(function() { return generateBundleTask.task({ workspace, diff --git a/lib/build/definitions/library.js b/lib/build/definitions/library.js index 2e8808498..985db4e1d 100644 --- a/lib/build/definitions/library.js +++ b/lib/build/definitions/library.js @@ -1,4 +1,5 @@ import {enhancePatternWithExcludes} from "./_utils.js"; +import {enhanceBundlesWithDefaults} from "../../validation/validator.js"; /** * Get tasks and their configuration for a given application project @@ -119,7 +120,10 @@ export default function({project, taskUtil, getTask}) { requiresDependencies: true, taskFunction: async ({workspace, dependencies, taskUtil, options}) => { const generateBundleTask = await getTask("generateBundle"); - return bundles.reduce(function(sequence, bundle) { + // Async resolve default values for bundle definitions and options + const bundlesDefaults = await enhanceBundlesWithDefaults(bundles, taskUtil.getProject()); + + return bundlesDefaults.reduce(function(sequence, bundle) { return sequence.then(function() { return generateBundleTask.task({ workspace, diff --git a/lib/validation/validator.js b/lib/validation/validator.js index d994d41fe..233e792cb 100644 --- a/lib/validation/validator.js +++ b/lib/validation/validator.js @@ -18,7 +18,7 @@ export const SCHEMA_VARIANTS = { }; class Validator { - constructor({Ajv, ajvErrors, schemaName}) { + 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( @@ -29,11 +29,12 @@ class Validator { this._schemaName = SCHEMA_VARIANTS[schemaName]; - this.ajv = new Ajv({ + ajvConfig = Object.assign({ allErrors: true, jsonPointers: true, loadSchema: Validator.loadSchema - }); + }, ajvConfig); + this.ajv = new Ajv(ajvConfig); ajvErrors(this.ajv); } @@ -77,6 +78,7 @@ class Validator { } const validator = Object.create(null); +const defaultsValidator = Object.create(null); async function _validate(schemaName, options) { if (!validator[schemaName]) { @@ -91,6 +93,27 @@ async function _validate(schemaName, options) { 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. * @@ -114,6 +137,52 @@ 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} 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} 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. * diff --git a/test/lib/build/definitions/application.js b/test/lib/build/definitions/application.js index d78f9313d..ec0a85d88 100644 --- a/test/lib/build/definitions/application.js +++ b/test/lib/build/definitions/application.js @@ -31,13 +31,14 @@ function getMockProject() { } test.beforeEach((t) => { + t.context.project = getMockProject(); t.context.taskUtil = { + getProject: sinon.stub().returns(t.context.project), isRootProject: sinon.stub().returns(true), getBuildOption: sinon.stub(), getInterface: sinon.stub() }; - t.context.project = getMockProject(); t.context.getTask = sinon.stub(); }); @@ -198,12 +199,14 @@ test("Custom bundles", async (t) => { "project/b/sectionsA/", "!project/b/sectionsA/section2**", ] - }], - sort: true + }] }, bundleOptions: { optimize: true, - usePredefinedCalls: true + usePredefineCalls: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, } }, { bundleDefinition: { @@ -215,12 +218,14 @@ test("Custom bundles", async (t) => { "project/b/sectionsB/", "!project/b/sectionsB/section2**", ] - }], - sort: true + }] }, bundleOptions: { optimize: false, - usePredefinedCalls: true + usePredefineCalls: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, } }]; @@ -322,13 +327,20 @@ test("Custom bundles", async (t) => { filters: [ "project/b/sectionsA/", "!project/b/sectionsA/section2**", - ] - }], - sort: true + ], + declareRawModules: false, + renderer: false, + resolve: false, + resolveConditional: false, + sort: true, + }] }, bundleOptions: { optimize: true, - usePredefinedCalls: true + usePredefineCalls: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, } } }, "generateBundle task got called with correct arguments"); @@ -346,13 +358,20 @@ test("Custom bundles", async (t) => { filters: [ "project/b/sectionsB/", "!project/b/sectionsB/section2**", - ] - }], - sort: true + ], + declareRawModules: false, + renderer: false, + resolve: false, + resolveConditional: false, + sort: true, + }] }, bundleOptions: { optimize: false, - usePredefinedCalls: true + usePredefineCalls: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, } } }, "generateBundle task got called with correct arguments"); @@ -415,12 +434,14 @@ test("generateComponentPreload with custom paths, excludes and custom bundle", ( "project/b/sectionsA/", "!project/b/sectionsA/section2**", ] - }], - sort: true + }] }, bundleOptions: { optimize: true, - usePredefinedCalls: true + usePredefineCalls: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, } }]; diff --git a/test/lib/build/definitions/library.js b/test/lib/build/definitions/library.js index 510ecb548..1864aa500 100644 --- a/test/lib/build/definitions/library.js +++ b/test/lib/build/definitions/library.js @@ -34,13 +34,14 @@ function getMockProject() { } test.beforeEach((t) => { + t.context.project = getMockProject(); t.context.taskUtil = { + getProject: sinon.stub().returns(t.context.project), isRootProject: sinon.stub().returns(true), getBuildOption: sinon.stub(), getInterface: sinon.stub() }; - t.context.project = getMockProject(); t.context.getTask = sinon.stub(); }); @@ -277,12 +278,14 @@ test("Custom bundles", async (t) => { "project/b/sectionsA/", "!project/b/sectionsA/section2**", ] - }], - sort: true + }] }, bundleOptions: { optimize: true, - usePredefinedCalls: true + usePredefineCalls: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, } }, { bundleDefinition: { @@ -294,12 +297,14 @@ test("Custom bundles", async (t) => { "project/b/sectionsB/", "!project/b/sectionsB/section2**", ] - }], - sort: true + }] }, bundleOptions: { optimize: false, - usePredefinedCalls: true + usePredefineCalls: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, } }]; @@ -415,13 +420,20 @@ test("Custom bundles", async (t) => { filters: [ "project/b/sectionsA/", "!project/b/sectionsA/section2**", - ] - }], - sort: true + ], + declareRawModules: false, + renderer: false, + resolve: false, + resolveConditional: false, + sort: true, + }] }, bundleOptions: { optimize: true, - usePredefinedCalls: true + usePredefineCalls: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, } } }, "generateBundle task got called with correct arguments"); @@ -439,13 +451,20 @@ test("Custom bundles", async (t) => { filters: [ "project/b/sectionsB/", "!project/b/sectionsB/section2**", - ] - }], - sort: true + ], + declareRawModules: false, + renderer: false, + resolve: false, + resolveConditional: false, + sort: true, + }] }, bundleOptions: { optimize: false, - usePredefinedCalls: true + usePredefineCalls: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, } } }, "generateBundle task got called with correct arguments"); @@ -508,12 +527,14 @@ test("generateComponentPreload with custom paths, excludes and custom bundle", ( "project/b/sectionsA/", "!project/b/sectionsA/section2**", ] - }], - sort: true + }] }, bundleOptions: { optimize: true, - usePredefinedCalls: true + usePredefineCalls: true, + addTryCatchRestartWrapper: false, + decorateBootstrapModule: true, + numberOfParts: 1, } }];