diff --git a/packages/project/test/lib/build/ProjectBuilder.bundling.integration.js b/packages/project/test/lib/build/ProjectBuilder.bundling.integration.js
new file mode 100644
index 00000000000..ee506da9b69
--- /dev/null
+++ b/packages/project/test/lib/build/ProjectBuilder.bundling.integration.js
@@ -0,0 +1,591 @@
+import test from "ava";
+import fs from "node:fs/promises";
+import {createFixtureTesterFactory, registerBuildHooks} from "./__helper__/ProjectBuilderFixtureTester.js";
+
+const FixtureTester = createFixtureTesterFactory("bundling");
+registerBuildHooks(test);
+
+test.serial("Build application.a (JSDoc build)", async (t) => {
+ const fixtureTester = new FixtureTester(t, "application.a");
+ const destPath = fixtureTester.destPath;
+
+ // This test should cover a scenario with an application depending on a library.
+ // We're executing a JSDoc build including dependencies (as with "ui5 build jsdoc --all")
+ // and testing if the output contains the expected JSDoc contents.
+ // Then, we're adding some additional JSDoc annotations to the library
+ // and testing the same again.
+
+ // #1 build (no cache, no changes)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5.yaml"},
+ config: {destPath, cleanDest: true, jsdoc: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {}
+ }
+ }
+ });
+
+ // Check that JSDoc build ran successfully:
+ await t.throwsAsync(fs.readFile(`${destPath}/resources/library/d/some-dbg.js`, {encoding: "utf8"}));
+ await t.throwsAsync(fs.readFile(`${destPath}/resources/library/d/some.js.map`, {encoding: "utf8"}));
+ const builtFileContent = await fs.readFile(`${destPath}/resources/library/d/some.js`, {encoding: "utf8"});
+ // Check that output contains correct file content:
+ t.false(builtFileContent.includes(`//# sourceMappingURL=some.js.map`),
+ "Build dest does not contain source map reference");
+
+
+ // #2 build (with cache, no changes)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5.yaml"},
+ config: {destPath, cleanDest: true, jsdoc: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // Add additional JSDoc annotations to library.d:
+ const jsdocContent = `/*!
+* ` + "${copyright}" + `
+*/
+
+/**
+* Example JSDoc annotation
+*
+* @public
+* @static
+* @param {object} param
+* @returns {string} output
+*/
+function functionWithJSDoc(param) {return "test"}`;
+
+ await fs.writeFile(`${fixtureTester.fixturePath}/node_modules/library.d/main/src/library/d/some.js`,
+ jsdocContent);
+
+ // #3 build (no cache, with changes)
+ // application.a should get skipped:
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5.yaml"},
+ config: {destPath, cleanDest: true, jsdoc: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "library.d": {
+ skippedTasks: [
+ "buildThemes",
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "executeJsdocSdkTransformation",
+ ]
+ }
+ }
+ }
+ });
+
+ // Check that JSDoc build ran successfully:
+ await t.throwsAsync(fs.readFile(`${destPath}/resources/library/d/some-dbg.js`, {encoding: "utf8"}));
+ await t.throwsAsync(fs.readFile(`${destPath}/resources/library/d/some.js.map`, {encoding: "utf8"}));
+ const builtFileContent2 = await fs.readFile(`${destPath}/resources/library/d/some.js`, {encoding: "utf8"});
+ t.true(builtFileContent2.includes(`Example JSDoc annotation`), "Build dest contains new JSDoc content");
+ // Check that output contains new file content:
+ t.false(builtFileContent2.includes(`//# sourceMappingURL=some.js.map`),
+ "Build dest does not contain source map reference");
+
+
+ // #4 build (no cache, no changes)
+ // Normal build again (non-JSDoc build); should not execute task "generateJsdoc":
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5.yaml"},
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {}
+ }
+ }
+ });
+
+ // Check that normal build ran successfully:
+ await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/d/some-dbg.js`, {encoding: "utf8"}));
+ await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/d/some.js.map`, {encoding: "utf8"}));
+ const builtFileContent3 = await fs.readFile(`${destPath}/resources/library/d/some.js`, {encoding: "utf8"});
+ t.false(builtFileContent3.includes(`Example JSDoc annotation`), "Build dest doesn't contain JSDoc content anymore");
+ // Check that output contains content generated by the normal build:
+ t.true(builtFileContent3.includes(`//# sourceMappingURL=some.js.map`),
+ "Build dest does contain source map reference");
+});
+
+test.serial("Build application.a (Custom Component preload configuration)", async (t) => {
+ const fixtureTester = new FixtureTester(t, "application.a");
+ const destPath = fixtureTester.destPath;
+
+ // In this test, we're testing the behavior of a custom preload configuration
+ // which is defined in "ui5-custom-preload-config.yaml".
+ // This custom preload configuration generates a Component-preload.js similar to a default one.
+ // However, it will omit a resource ("scriptWithSourceMap.js") from the bundle.
+
+ // #1 build (no cache, no changes)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-custom-preload-config.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {}
+ }
+ }
+ });
+
+ // Check that generated preload bundle doesn't contain the omitted file:
+ t.false((await fs.readFile(`${destPath}/Component-preload.js`, {encoding: "utf8"}))
+ .includes("id1/thirdparty/scriptWithSourceMap.js"));
+
+
+ // #2 build (with cache, no changes)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-custom-preload-config.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}
+ }
+ });
+});
+
+test.serial("Build application.a (self-contained build)", async (t) => {
+ const fixtureTester = new FixtureTester(t, "application.a");
+ const destPath = fixtureTester.destPath;
+
+ // We're executing a self-contained build including dependencies (as with "ui5 build self-contained --all")
+ // and testing if the output contains the expected self-contained bundle.
+ // Then, we're changing the content only of application.a
+ // and testing if the self-contained build output changes accordingly.
+
+ // #1 build (no cache, no changes)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5.yaml"},
+ config: {destPath, cleanDest: true, selfContained: "self-contained",
+ dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {}
+ }
+ }
+ });
+
+ // Check that output contains the correct content:
+ const builtFileContent = await fs.readFile(`${destPath}/resources/sap-ui-custom.js`, {encoding: "utf8"});
+ t.true(builtFileContent.includes(`"id1/test.js":'function test(t){var o=t;console.log(o)}test();\\n'`));
+
+
+ // #2 build (with cache, no changes)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5.yaml"},
+ config: {destPath, cleanDest: true, selfContained: "self-contained",
+ dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // Remove the file "test.js" from application.a:
+ await fs.rm(`${fixtureTester.fixturePath}/webapp/test.js`);
+
+ // #3 build (with cache, with changes)
+ // Dependencies should get skipped, application.a should get rebuilt:
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5.yaml"},
+ config: {destPath, cleanDest: true, selfContained: "self-contained",
+ dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "application.a": {
+ skippedTasks: [
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "generateFlexChangesBundle",
+ "replaceCopyright",
+ "transformBootstrapHtml",
+ "generateVersionInfo",
+ ]
+ }
+ }
+ }
+ });
+
+ // Check that output contains the correct content (test.js should be missing):
+ const builtFileContent2 = await fs.readFile(`${destPath}/resources/sap-ui-custom.js`, {encoding: "utf8"});
+ t.false(builtFileContent2.includes(`"id1/test.js":`));
+
+
+ // #4 build (with cache, no changes)
+ // Run a self-contained build but with a different config which defines a custom preload.
+ // The build should run and the output should still contain the expected self-contained bundle
+ // (tasks "generateComponentPreload" and "generateLibraryPreload" should not get executed):
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-custom-preload-config.yaml"},
+ config: {destPath, cleanDest: true, selfContained: "self-contained",
+ dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "application.a": {}
+ }
+ }
+ });
+
+ // Check that output contains still the correct content:
+ const builtFileContent3 = await fs.readFile(`${destPath}/resources/sap-ui-custom.js`, {encoding: "utf8"});
+ t.false(builtFileContent3.includes(`"id1/test.js":`));
+
+
+ // #5 build (with cache, no changes)
+ // Run a self-contained build but without dependencies:
+ // (everything should get skipped)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5.yaml"},
+ config: {destPath, cleanDest: true, selfContained: "self-contained"},
+ assertions: {
+ projects: {}
+ }
+ });
+});
+
+test.serial("Build application.a (Custom bundling)", async (t) => {
+ const fixtureTester = new FixtureTester(t, "application.a");
+ const destPath = fixtureTester.destPath;
+
+ // In this test, we're testing the behavior of a custom bundling configuration
+ // which is defined in "ui5-custom-bundling.yaml".
+ // This config generates a custom bundle in various modes.
+ // The bundle includes resources by a filter ("Component.js" & "newFile.js") which are added at #3 and #4 build.
+
+ // #1 build with custom bundle configuration (with empty cache)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-custom-bundling.yaml"},
+ config: {destPath, cleanDest: false},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {}
+ }
+ }
+ });
+
+ // Verify that the custom bundle was created and contains the expected content:
+ const customBundlePath = `${destPath}/resources/custom-bundle.js`;
+ const customBundleContent = await fs.readFile(customBundlePath, {encoding: "utf8"});
+ t.false(customBundleContent.includes("sap.ui.predefine("),
+ "preload Mode: Custom bundle should not contain sap.ui.predefine() at this stage"
+ );
+ t.false(customBundleContent.includes("sap.ui.require.preload("),
+ "preload Mode: Custom bundle should not contain sap.ui.require.preload() at this stage"
+ );
+ // Verify that source map was created:
+ const sourceMapPath = `${destPath}/resources/custom-bundle.js.map`;
+ const sourceMapContent = JSON.parse(await fs.readFile(sourceMapPath, {encoding: "utf8"}));
+ t.true(sourceMapContent.sections.length === 0, "Source map file should not have content at this stage");
+
+
+ // #2 build with custom bundle (with cache, no changes)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-custom-bundling.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // Add a source file which is matched by the filter (UI5 Module):
+ const newComponentFilepath = `${fixtureTester.fixturePath}/webapp/Component.js`;
+ await fs.appendFile(newComponentFilepath,
+ `sap.ui.define(["sap/ui/core/UIComponent", "sap/ui/core/ComponentSupport"], (UIComponent) => {
+ "use strict";
+ return UIComponent.extend("id1.Component", {
+ metadata: {
+ manifest: "json",
+ interfaces: ["sap.ui.core.IAsyncContentCreation"],
+ }
+ });
+});`);
+
+ // #3 build with custom bundle (with cache, with changes)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-custom-bundling.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "application.a": {
+ skippedTasks: [
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "generateFlexChangesBundle",
+ "replaceCopyright",
+ "generateVersionInfo",
+ ]
+ }
+ }
+ }
+ });
+
+ // Verify that the updated custom bundle contains the change:
+ // (the bundle should now contain sap.ui.predefine() due to the added UI5 module "Component.js")
+ const customBundleContent2 = await fs.readFile(customBundlePath, {encoding: "utf8"});
+ t.true(customBundleContent2.includes("sap.ui.predefine("),
+ "preload Mode: Custom bundle should contain sap.ui.predefine() now"
+ );
+ t.false(customBundleContent2.includes("sap.ui.require.preload("),
+ "preload Mode: Custom bundle should not contain sap.ui.require.preload() at this stage"
+ );
+ // Verify that source map was created and contains the change:
+ const sourceMapContent2 = JSON.parse(await fs.readFile(sourceMapPath, {encoding: "utf8"}));
+ t.true(sourceMapContent2.sections.length > 0, "Source map file should have content now");
+
+
+ // Add another source file which is matched by the filter (non-UI5 module):
+ const newTestFilepath = `${fixtureTester.fixturePath}/webapp/newFile.js`;
+ await fs.appendFile(newTestFilepath, `console.log("another source file");`);
+
+ // #4 build with custom bundle (with cache, with changes)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-custom-bundling.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "application.a": {
+ skippedTasks: [
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "generateFlexChangesBundle",
+ "replaceCopyright",
+ "generateVersionInfo",
+ ]
+ }
+ }
+ }
+ });
+
+ // Verify that the custom bundle was created and contains the expected content:
+ // (the bundle should now contain sap.ui.require.preload() due to the added non-UI5 module "newFile.js")
+ const customBundleContent3 = await fs.readFile(customBundlePath, {encoding: "utf8"});
+ t.true(customBundleContent3.includes("sap.ui.predefine("),
+ "preload Mode: Custom bundle should contain sap.ui.predefine() still"
+ );
+ t.true(customBundleContent3.includes("sap.ui.require.preload("),
+ "preload Mode: Custom bundle should contain sap.ui.require.preload() now"
+ );
+ // Verify that source map was created and contains the change:
+ const sourceMapContent3 = JSON.parse(await fs.readFile(sourceMapPath, {encoding: "utf8"}));
+ t.true(sourceMapContent3.sections.length > 0, "Source map file should have content still");
+
+
+ // ----------------------------------------------------------------------------------
+ // ---------------------------- Test other bundle modes: ----------------------------
+ // ----------------------------------------------------------------------------------
+ // Switch to "raw" mode:
+ const ui5YamlContent = await fs.readFile(`${fixtureTester.fixturePath}/ui5-custom-bundling.yaml`);
+ await fs.writeFile(`${fixtureTester.fixturePath}/ui5-custom-bundling.yaml`,
+ ui5YamlContent.toString().replace(`- mode: preload`, `- mode: raw`));
+
+ // #5 build with custom bundle configuration (with empty cache)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-custom-bundling.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "application.a": {}
+ }
+ }
+ });
+
+ // Verify that the custom bundle was created and contains the expected content:
+ // (the bundle should contain sap.ui.define() now)
+ const customBundleContent4 = await fs.readFile(customBundlePath, {encoding: "utf8"});
+ t.false(customBundleContent4.includes("sap.ui.require.preload("),
+ "raw Mode: Custom bundle should not contain sap.ui.require.preload() anymore"
+ );
+ t.false(customBundleContent4.includes("sap.ui.predefine("),
+ "raw Mode: Custom bundle should not contain sap.ui.predefine() anymore"
+ );
+ t.true(customBundleContent4.includes("sap.ui.define("),
+ "raw Mode: Custom bundle should contain sap.ui.define() now"
+ );
+ t.true(customBundleContent4.includes(`console.log("another source file");`));
+ t.true(customBundleContent4.includes(`id1.Component`));
+ // Verify that source map was created and contains the change:
+ const sourceMapContent4 = JSON.parse(await fs.readFile(sourceMapPath, {encoding: "utf8"}));
+ t.true(sourceMapContent4.sections.length > 0, "Source map file should have content still");
+
+
+ // Switch to "require" mode:
+ const ui5YamlContent2 = await fs.readFile(`${fixtureTester.fixturePath}/ui5-custom-bundling.yaml`);
+ await fs.writeFile(`${fixtureTester.fixturePath}/ui5-custom-bundling.yaml`,
+ ui5YamlContent2.toString().replace(`- mode: raw`, `- mode: require`));
+
+ // #6 build with custom bundle configuration (with empty cache)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-custom-bundling.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "application.a": {}
+ }
+ }
+ });
+
+ // Verify that the custom bundle was created and contains the expected content:
+ // (the bundle should contain sap.ui.require() now)
+ const customBundleContent5 = await fs.readFile(customBundlePath, {encoding: "utf8"});
+ t.false(customBundleContent5.includes("sap.ui.require.preload("),
+ "require Mode: Custom bundle should not contain sap.ui.require.preload() anymore"
+ );
+ t.false(customBundleContent5.includes("sap.ui.predefine("),
+ "require Mode: Custom bundle should not contain sap.ui.predefine() anymore"
+ );
+ t.false(customBundleContent5.includes("sap.ui.define("),
+ "require Mode: Custom bundle should not contain sap.ui.define() anymore"
+ );
+ t.true(customBundleContent5.includes("sap.ui.require("),
+ "require Mode: Custom bundle should contain sap.ui.require() now"
+ );
+ t.true(customBundleContent5.includes(`id1/newFile`));
+ t.false(customBundleContent5.includes(`console.log("another source file");`));
+ t.true(customBundleContent5.includes(`id1/Component`));
+ // Verify that source map was created and contains the change:
+ const sourceMapContent5 = JSON.parse(await fs.readFile(sourceMapPath, {encoding: "utf8"}));
+ t.true(sourceMapContent5.sections.length > 0, "Source map file should have content still");
+
+
+ // Switch to "bundleInfo" mode:
+ const ui5YamlContent3 = await fs.readFile(`${fixtureTester.fixturePath}/ui5-custom-bundling.yaml`);
+ await fs.writeFile(`${fixtureTester.fixturePath}/ui5-custom-bundling.yaml`,
+ ui5YamlContent3.toString().replace(`- mode: require`, `- mode: bundleInfo`));
+
+ // #7 build with custom bundle configuration (with empty cache)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-custom-bundling.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "application.a": {}
+ }
+ }
+ });
+
+ // Verify that the custom bundle was created and contains the expected content:
+ // (the bundle should contain sap.ui.loader.config() now)
+ const customBundleContent6 = await fs.readFile(customBundlePath, {encoding: "utf8"});
+ t.false(customBundleContent6.includes("sap.ui.require.preload("),
+ "bundleInfo Mode: Custom bundle should not contain sap.ui.require.preload() anymore"
+ );
+ t.false(customBundleContent6.includes("sap.ui.predefine("),
+ "bundleInfo Mode: Custom bundle should not contain sap.ui.predefine() anymore"
+ );
+ t.false(customBundleContent6.includes("sap.ui.define("),
+ "bundleInfo Mode: Custom bundle should not contain sap.ui.define() anymore"
+ );
+ t.false(customBundleContent6.includes("sap.ui.require("),
+ "bundleInfo Mode: Custom bundle should not contain sap.ui.require() anymore"
+ );
+ t.true(customBundleContent6.includes("sap.ui.loader.config({bundlesUI5:{"),
+ "bundleInfo Mode: Custom bundle should contain sap.ui.loader.config() now"
+ );
+ t.true(customBundleContent6.includes(`id1/newFile`));
+ t.false(customBundleContent6.includes(`console.log("another source file");`));
+ t.true(customBundleContent6.includes(`id1/Component`));
+ // Verify that source map was created and contains the change:
+ const sourceMapContent6 = JSON.parse(await fs.readFile(sourceMapPath, {encoding: "utf8"}));
+ t.true(sourceMapContent6.sections.length > 0, "Source map file should have content still");
+});
+
+test.serial("Build library.d (Custom Library preload configuration)", async (t) => {
+ const fixtureTester = new FixtureTester(t, "library.d");
+ const destPath = fixtureTester.destPath;
+
+ // In this test, we're testing the behavior of a custom preload configuration
+ // which is defined in "ui5-custom-preload-config.yaml".
+ // This custom preload configuration generates a library-preload.js similar to a default one.
+ // However, it will omit a resource ("some.js") from the bundle.
+
+ // ui5-custom-preload-config.yaml uses specVersion 5.0, which requires a manifest.json
+ await fixtureTester._initialize();
+ await fs.writeFile(`${fixtureTester.fixturePath}/main/src/library/d/manifest.json`,
+ JSON.stringify({"sap.app": {"id": "library.d", "type": "library"}}, null, "\t"));
+
+ // #1 build (no cache, no changes)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-custom-preload-config.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "library.d": {}
+ }
+ }
+ });
+
+ // Check that generated preload bundle doesn't contain the omitted file:
+ t.false((await fs.readFile(`${destPath}/resources/library/d/library-preload.js`, {encoding: "utf8"}))
+ .includes("library/d/some.js"));
+
+
+ // #2 build (with cache, no changes)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-custom-preload-config.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}
+ }
+ });
+});
+
+test.serial("Build component.a (Custom Component preload configuration)", async (t) => {
+ const fixtureTester = new FixtureTester(t, "component.a");
+ const destPath = fixtureTester.destPath;
+
+ // In this test, we're testing the behavior of a custom preload configuration
+ // which is defined in "ui5-custom-preload-config.yaml".
+ // This custom preload configuration generates a Component-preload.js similar to a default one.
+ // However, it will omit a resource ("test.js") from the bundle.
+
+ // #1 build (no cache, no changes)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-custom-preload-config.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "component.a": {}
+ }
+ }
+ });
+
+ // Check that generated preload bundle doesn't contain the omitted file:
+ t.false((await fs.readFile(`${destPath}/resources/id1/Component-preload.js`, {encoding: "utf8"}))
+ .includes("id1/test.js"));
+
+
+ // #2 build (with cache, no changes)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-custom-preload-config.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}
+ }
+ });
+});
diff --git a/packages/project/test/lib/build/ProjectBuilder.cacheModes.integration.js b/packages/project/test/lib/build/ProjectBuilder.cacheModes.integration.js
new file mode 100644
index 00000000000..b02700c03c7
--- /dev/null
+++ b/packages/project/test/lib/build/ProjectBuilder.cacheModes.integration.js
@@ -0,0 +1,262 @@
+import test from "ava";
+import fs from "node:fs/promises";
+import Cache from "../../../lib/build/cache/Cache.js";
+import {createFixtureTesterFactory, registerBuildHooks} from "./__helper__/ProjectBuilderFixtureTester.js";
+
+const FixtureTester = createFixtureTesterFactory("cacheModes");
+registerBuildHooks(test);
+
+test.serial("Build application.a with --cache=Default", async (t) => {
+ const fixtureTester = new FixtureTester(t, "application.a");
+ const destPath = fixtureTester.destPath;
+
+ // #1 Build with empty cache --> all tasks execute
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: false, cache: Cache.Default},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {}
+ }
+ }
+ });
+
+ // #2 Build with valid cache, no changes --> nothing rebuilds (all cached)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, cache: Cache.Default},
+ assertions: {
+ projects: {}
+ }
+ });
+
+ // Change a source file in application.a
+ const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`;
+ await fs.appendFile(changedFilePath, `\ntest("line added for cache test");\n`);
+
+ // #3 Build with valid cache, source changes --> only affected tasks rebuild
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, cache: Cache.Default},
+ assertions: {
+ projects: {
+ "application.a": {
+ skippedTasks: [
+ "escapeNonAsciiCharacters",
+ "replaceCopyright",
+ "enhanceManifest",
+ "generateFlexChangesBundle",
+ "generateVersionInfo",
+ ]
+ }
+ }
+ }
+ });
+
+ // Verify the changed file is in the destPath
+ const builtFileContent = await fs.readFile(`${destPath}/test.js`, {encoding: "utf8"});
+ t.true(builtFileContent.includes(`test("line added for cache test");`),
+ "Build dest contains changed file content");
+});
+
+test.serial("Build application.a with --cache=Off", async (t) => {
+ const fixtureTester = new FixtureTester(t, "application.a");
+ const destPath = fixtureTester.destPath;
+
+ // #1 Build with cache=Off --> all tasks execute, cache not written
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: false, cache: Cache.Off},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {},
+ }
+ }
+ });
+
+ // #2 Build with cache=Off (again) --> all tasks execute again (no cache reuse)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, cache: Cache.Off},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {},
+ }
+ }
+ });
+
+ // #3 Build with cache=Default --> all tasks execute (no cache from previous builds)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, cache: Cache.Default},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {},
+ }
+ }
+ });
+
+ // #4 Build with cache=Default (again) --> nothing rebuilds (cache now exists)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, cache: Cache.Default},
+ assertions: {
+ projects: {}
+ }
+ });
+
+ // #5 Build with cache=Off --> all tasks execute (ignores existing cache)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, cache: Cache.Off},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {},
+ }
+ }
+ });
+});
+
+test.serial("Build application.a with --cache=ReadOnly", async (t) => {
+ const fixtureTester = new FixtureTester(t, "application.a");
+ const destPath = fixtureTester.destPath;
+
+ // #1 Build with cache=Default --> all tasks execute, cache written
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: false, cache: Cache.Default},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {}
+ }
+ }
+ });
+
+ // #2 Build with cache=ReadOnly, no changes --> nothing rebuilds (cache used)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, cache: Cache.ReadOnly},
+ assertions: {
+ projects: {}
+ }
+ });
+
+ // Change a source file in application.a
+ const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`;
+ await fs.appendFile(changedFilePath, `\ntest("line added for ReadOnly test");\n`);
+
+ // #3 Build with cache=ReadOnly --> affected tasks rebuild, BUT cache not updated
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, cache: Cache.ReadOnly},
+ assertions: {
+ projects: {
+ "application.a": {
+ skippedTasks: [
+ "escapeNonAsciiCharacters",
+ "replaceCopyright",
+ "enhanceManifest",
+ "generateFlexChangesBundle",
+ "generateVersionInfo",
+ ]
+ }
+ }
+ }
+ });
+
+ // Verify the changed file is in the destPath
+ const builtFileContent = await fs.readFile(`${destPath}/test.js`, {encoding: "utf8"});
+ t.true(builtFileContent.includes(`test("line added for ReadOnly test");`),
+ "Build dest contains changed file content");
+
+ // #4 Build with cache=Default, no new changes --> rebuilds again (cache from #3 missing)
+ // This validates that ReadOnly didn't write the cache in step #3
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, cache: Cache.Default},
+ assertions: {
+ projects: {
+ "application.a": {
+ skippedTasks: [
+ "escapeNonAsciiCharacters",
+ "replaceCopyright",
+ "enhanceManifest",
+ "generateFlexChangesBundle",
+ "generateVersionInfo",
+ ]
+ }
+ }
+ }
+ });
+});
+
+test.serial("Build application.a with --cache=Force (1)", async (t) => {
+ const fixtureTester = new FixtureTester(t, "application.a");
+ const destPath = fixtureTester.destPath;
+
+ // #1: Build with cache=Default --> all tasks execute, cache written
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: false, cache: Cache.Default},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {}
+ }
+ }
+ });
+
+ // #2: Build with cache=Force, no changes --> nothing rebuilds (cache used)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, cache: Cache.Force},
+ assertions: {
+ projects: {}
+ }
+ });
+
+ // Change a source file in application.a
+ const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`;
+ await fs.appendFile(changedFilePath, `\ntest("line added for Force test");\n`);
+
+ // #3: Build with cache=Force --> ERROR (cache invalid due to source changes)
+ const error = await t.throwsAsync(async () => {
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, cache: Cache.Force},
+ });
+ });
+
+ t.truthy(error, "Build with Force mode should throw error when cache is stale");
+ t.true(error.message.includes(`Cache is in "Force" mode but cache is stale for project application.a ` +
+ `due to 1 changed source file(s). ` +
+ `Use "Default", "ReadOnly" or "Off" to rebuild.`));
+});
+
+test.serial("Build application.a with --cache=Force (2)", async (t) => {
+ const fixtureTester = new FixtureTester(t, "application.a");
+ const destPath = fixtureTester.destPath;
+
+ // #1: Build with cache=Force on empty cache --> ERROR with clear message
+ const error = await t.throwsAsync(async () => {
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: false, cache: Cache.Force},
+ });
+ });
+
+ t.truthy(error, "Build with Force mode should throw error when cache is empty");
+ t.true(error.message.includes(`Cache is in "Force" mode but no cache found for project application.a. ` +
+ `Use "Default", "ReadOnly" or "Off" to rebuild.`));
+});
diff --git a/packages/project/test/lib/build/ProjectBuilder.caching.integration.js b/packages/project/test/lib/build/ProjectBuilder.caching.integration.js
new file mode 100644
index 00000000000..d1f18ef21c7
--- /dev/null
+++ b/packages/project/test/lib/build/ProjectBuilder.caching.integration.js
@@ -0,0 +1,756 @@
+import test from "ava";
+import fs from "node:fs/promises";
+import {createFixtureTesterFactory, registerBuildHooks} from "./__helper__/ProjectBuilderFixtureTester.js";
+
+const FixtureTester = createFixtureTesterFactory("caching");
+registerBuildHooks(test);
+
+test.serial("Build application.a project multiple times", async (t) => {
+ const fixtureTester = new FixtureTester(t, "application.a");
+ const destPath = fixtureTester.destPath;
+
+ // #1 build (with empty cache)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: false},
+ assertions: {
+ projects: {
+ // Default builds of project type "application" include the "generateVersionInfo"
+ // task which requires dependencies to be built, so all dependencies are expected to be built here.
+ // Subsequent builds can reuse the cached results of these dependencies.
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {}
+ }
+ }
+ });
+
+
+ // #2 build (with cache, no changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // Change a source file in application.a
+ const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`;
+ await fs.appendFile(changedFilePath, `\ntest("line added");\n`);
+
+ // #3 build (with cache, with changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "application.a": {
+ skippedTasks: [
+ "escapeNonAsciiCharacters",
+ // Note: replaceCopyright is skipped because no copyright is configured in the project
+ "replaceCopyright",
+ "enhanceManifest",
+ "generateFlexChangesBundle",
+ "generateVersionInfo",
+ ]
+ }
+ }
+ }
+ });
+
+ // Check whether the changed file is in the destPath
+ const builtFileContent = await fs.readFile(`${destPath}/test.js`, {encoding: "utf8"});
+ t.true(builtFileContent.includes(`test("line added");`), "Build dest contains changed file content");
+
+
+ // #4 build (with cache, no changes, with dependencies)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ // Dependencies are NOT rebuilt because
+ // they were already built in build #1 and can be reused from cache.
+ // Thus, empty assertion for built projects.
+ projects: {}
+ }
+ });
+
+
+ // #5 build (with cache, no changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // #6 build (with cache, no changes, with dependencies)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // #7 build (with cache, no changes, with custom tasks)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-customTask.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "application.a": {}
+ }
+ }
+ });
+
+
+ // #8 build (with cache, no changes, with custom tasks)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-customTask.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // #9 build (with cache, no changes, with dependencies)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // Change a source file with existing source map in application.a
+ const fileWithSourceMapPath =
+ `${fixtureTester.fixturePath}/webapp/thirdparty/scriptWithSourceMap.js`;
+ const fileWithSourceMapContent = await fs.readFile(fileWithSourceMapPath, {encoding: "utf8"});
+ await fs.writeFile(
+ fileWithSourceMapPath,
+ fileWithSourceMapContent.replace(
+ `This is a script with a source map.`,
+ `This is a CHANGED script with a source map.`
+ )
+ );
+ const sourceMapPath = `${fixtureTester.fixturePath}/webapp/thirdparty/scriptWithSourceMap.js.map`;
+ const sourceMapContent = await fs.readFile(sourceMapPath, {encoding: "utf8"});
+ await fs.writeFile(
+ sourceMapPath,
+ sourceMapContent.replace(
+ `This is a script with a source map.`,
+ `This is a CHANGED script with a source map.`
+ )
+ );
+
+ // #10 build (with cache, with changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "application.a": {
+ skippedTasks: [
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "generateFlexChangesBundle",
+ "generateVersionInfo",
+ "replaceCopyright"
+ ]
+ }
+ }
+ }
+ });
+
+
+ // Add a new file to application.a
+ await fs.writeFile(`${fixtureTester.fixturePath}/webapp/someNew.js`,
+ `console.log("SOME NEW CONTENT");\n`
+ );
+
+ // #11 build (with cache, with changes - someNew.js added)
+ // Tasks that don't depend on someNew.js can reuse their caches from build #10.
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {"application.a": {
+ skippedTasks: [
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "generateFlexChangesBundle",
+ "replaceCopyright",
+ "generateVersionInfo",
+ ]
+ }}
+ }
+ });
+
+ await fs.rm(`${fixtureTester.fixturePath}/webapp/someNew.js`);
+
+ // #12 build (with cache, with changes - someNew.js removed)
+ // Source state matches build #10's cached result -> cache reused, everything skipped
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {},
+ }
+ });
+});
+
+test.serial("Build library.d project multiple times", async (t) => {
+ const fixtureTester = new FixtureTester(t, "library.d");
+ const destPath = fixtureTester.destPath;
+
+ // #1 build (with empty cache)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: false},
+ assertions: {
+ projects: {"library.d": {}}
+ }
+ });
+
+
+ // #2 build (with cache, no changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // Change a source file in library.d
+ const changedFilePath = `${fixtureTester.fixturePath}/main/src/library/d/.library`;
+ await fs.writeFile(
+ changedFilePath,
+ (await fs.readFile(changedFilePath, {encoding: "utf8"})).replace(
+ `Library D`,
+ `Library D (updated #1)`
+ )
+ );
+
+ // #3 build (with cache, with changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {"library.d": {
+ skippedTasks: [
+ "buildThemes",
+ "escapeNonAsciiCharacters",
+ "minify",
+ "replaceBuildtime",
+ ]
+ }}
+ }
+ });
+
+ // Check whether the changes are in the destPath
+ const builtFileContent = await fs.readFile(`${destPath}/resources/library/d/.library`, {encoding: "utf8"});
+ t.true(
+ builtFileContent.includes(`Library D (updated #1)`),
+ "Build dest contains changed file content"
+ );
+
+ // Check whether the manifest.json was updated with the new documentation
+ const manifestContent = await fs.readFile(`${destPath}/resources/library/d/manifest.json`, {encoding: "utf8"});
+ t.true(
+ manifestContent.includes(`"Library D (updated #1)"`),
+ "Build dest contains updated description in manifest.json"
+ );
+
+
+ // #4 build (with cache, no changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // Update copyright in ui5.yaml (should trigger a full rebuild of the project)
+ const ui5YamlPath = `${fixtureTester.fixturePath}/ui5.yaml`;
+ await fs.writeFile(
+ ui5YamlPath,
+ (await fs.readFile(ui5YamlPath, {encoding: "utf8"})).replace(
+ "copyright: Some fancy copyright",
+ "copyright: Some updated fancy copyright"
+ )
+ );
+
+ await fs.writeFile(`${fixtureTester.fixturePath}/main/src/library/d/someNew.js`,
+ `console.log("SOME NEW CONTENT");\n`
+ );
+
+ // #5 build (with cache, with changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {"library.d": {}}
+ }
+ });
+
+ await fs.rm(`${fixtureTester.fixturePath}/main/src/library/d/someNew.js`);
+
+ // #6 build (with cache, with changes - someNew.js removed)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {"library.d": {
+ skippedTasks: [
+ "buildThemes",
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "replaceBuildtime",
+ ]
+ }},
+ }
+ });
+
+ // Re-add someNew.js (restores source state to match build #5)
+ await fs.writeFile(`${fixtureTester.fixturePath}/main/src/library/d/someNew.js`,
+ `console.log("SOME NEW CONTENT");\n`
+ );
+
+ // #7 build (with cache, with changes - someNew.js re-added)
+ // Source state now matches build #5's cached result -> cache reused
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {},
+ }
+ });
+
+ // Remove someNew.js again
+ await fs.rm(`${fixtureTester.fixturePath}/main/src/library/d/someNew.js`);
+
+ // #8 build (with cache, with changes - someNew.js removed again)
+ // Source state matches build #6's cached result -> cache reused, everything skipped
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {},
+ }
+ });
+});
+
+test.serial("Build theme.library.e project multiple times", async (t) => {
+ const fixtureTester = new FixtureTester(t, "theme.library.e");
+ const destPath = fixtureTester.destPath;
+
+ // #1 build (with empty cache)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: false},
+ assertions: {
+ projects: {"theme.library.e": {}}
+ }
+ });
+
+
+ // #2 build (with cache, no changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // Change a source file in theme.library.e
+ const librarySourceFilePath =
+ `${fixtureTester.fixturePath}/src/theme/library/e/themes/my_theme/library.source.less`;
+ await fs.appendFile(librarySourceFilePath, `\n.someNewClass {\n\tcolor: red;\n}\n`);
+
+ // #3 build (with cache, with changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {"theme.library.e": {}}
+ }
+ });
+
+ // Check whether the changed file is in the destPath
+ const builtFileContent = await fs.readFile(
+ `${destPath}/resources/theme/library/e/themes/my_theme/library.source.less`, {encoding: "utf8"}
+ );
+ t.true(
+ builtFileContent.includes(`.someNewClass`),
+ "Build dest contains changed file content"
+ );
+
+ // Check whether the build output contains the new CSS rule
+ const builtCssContent = await fs.readFile(
+ `${destPath}/resources/theme/library/e/themes/my_theme/library.css`, {encoding: "utf8"}
+ );
+ t.true(
+ builtCssContent.includes(`.someNewClass`),
+ "Build dest contains new rule in library.css"
+ );
+
+
+ // Add a new less file and import it in library.source.less
+ await fs.writeFile(`${fixtureTester.fixturePath}/src/theme/library/e/themes/my_theme/newImportFile.less`,
+ `.someOtherNewClass {\n\tcolor: blue;\n}\n`
+ );
+ await fs.appendFile(librarySourceFilePath, `\n@import "newImportFile.less";\n`);
+
+ // #4 build (with cache, with changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {"theme.library.e": {}},
+ }
+ });
+
+ // Check whether the build output contains the import to the new file
+ const builtCssContent2 = await fs.readFile(
+ `${destPath}/resources/theme/library/e/themes/my_theme/library.css`, {encoding: "utf8"}
+ );
+ t.true(
+ builtCssContent2.includes(`.someOtherNewClass`),
+ "Build dest contains new rule in library.css"
+ );
+
+
+ // #5 build (with cache, no changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {},
+ }
+ });
+
+
+ // Change content of new less file
+ await fs.writeFile(`${fixtureTester.fixturePath}/src/theme/library/e/themes/my_theme/newImportFile.less`,
+ `.someOtherNewClass {\n\tcolor: green;\n}\n`
+ );
+
+ // #6 build (with cache, with changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {"theme.library.e": {}},
+ }
+ });
+
+ // Check whether the build output contains the changed content of the imported file
+ const builtCssContent3 = await fs.readFile(
+ `${destPath}/resources/theme/library/e/themes/my_theme/library.css`, {encoding: "utf8"}
+ );
+ t.true(
+ builtCssContent3.includes(`.someOtherNewClass{color:green}`),
+ "Build dest contains new rule in library.css"
+ );
+
+
+ // Delete import of library.source.less
+ const librarySourceFileContent = (await fs.readFile(librarySourceFilePath)).toString();
+ await fs.writeFile(librarySourceFilePath,
+ librarySourceFileContent.replace(`\n@import "newImportFile.less";\n`, "")
+ );
+
+ // Change content of new less file again
+ await fs.writeFile(`${fixtureTester.fixturePath}/src/theme/library/e/themes/my_theme/newImportFile.less`,
+ `.someOtherNewClass {\n\tcolor: yellow;\n}\n`
+ );
+
+ // #7 build (with cache, with changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {"theme.library.e": {
+ skippedTasks: ["buildThemes"]
+ }},
+ }
+ });
+
+ // Check if library.css does NOT contain the imported rule anymore
+ t.false(
+ (await fs.readFile(
+ `${destPath}/resources/theme/library/e/themes/my_theme/library.css`, {encoding: "utf8"}
+ )).includes(`.someOtherNewClass`),
+ "Build dest should NOT contain the rule in library.css anymore"
+ );
+
+
+ // Delete the imported less file
+ await fs.rm(`${fixtureTester.fixturePath}/src/theme/library/e/themes/my_theme/newImportFile.less`);
+
+ // #8 build (with cache, with changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}, // -> everything should be skipped
+ }
+ });
+});
+
+test.serial("Build component.a project multiple times", async (t) => {
+ const fixtureTester = new FixtureTester(t, "component.a");
+ const destPath = fixtureTester.destPath;
+
+ // #1 build (no cache, no changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "component.a": {}
+ }
+ }
+ });
+
+
+ // #2 build (with cache, no changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // Change a source file in component.a
+ const changedFilePath = `${fixtureTester.fixturePath}/src/test.js`;
+ await fs.appendFile(changedFilePath, `\ntest("line added");\n`);
+
+ // #3 build (with cache, with changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "component.a": {
+ skippedTasks: [
+ "escapeNonAsciiCharacters",
+ // Note: replaceCopyright is skipped because no copyright is configured in the project
+ "replaceCopyright",
+ "enhanceManifest",
+ "generateFlexChangesBundle",
+ ]
+ }
+ }
+ }
+ });
+
+ // Check whether the changed file is in the destPath
+ const builtFileContent = await fs.readFile(`${destPath}/resources/id1/test.js`, {encoding: "utf8"});
+ t.true(builtFileContent.includes(`test("line added");`), "Build dest contains changed file content");
+
+
+ // #4 build (with cache, no changes, with dependencies)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ }
+ }
+ });
+
+
+ // #5 build (with cache, no changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // #6 build (with cache, no changes, with dependencies)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // Add a new file to component.a
+ await fs.writeFile(`${fixtureTester.fixturePath}/src/someNew.js`,
+ `console.log("SOME NEW CONTENT");\n`
+ );
+
+ // #7 build (with cache, with changes - someNew.js added)
+ // Tasks that don't depend on someNew.js can reuse their caches from build #3.
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {"component.a": {
+ skippedTasks: [
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "generateFlexChangesBundle",
+ "replaceCopyright",
+ ]
+ }}
+ }
+ });
+
+ await fs.rm(`${fixtureTester.fixturePath}/src/someNew.js`);
+
+ // #8 build (with cache, with changes - someNew.js removed)
+ // Source state matches build #6's cached result -> cache reused, everything skipped
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {},
+ }
+ });
+});
+
+test.serial("Build module.b project multiple times", async (t) => {
+ const fixtureTester = new FixtureTester(t, "module.b");
+ const destPath = fixtureTester.destPath;
+
+ // #1 build (no cache, no changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {"module.b": {}}
+ },
+ });
+
+
+ // #2 build (with cache, no changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // Change a source file in module.b
+ const changedFilePath = `${fixtureTester.fixturePath}/dev/devTools.js`;
+ await fs.appendFile(changedFilePath, `\ntest("line added");\n`);
+
+ // #3 build (no cache, with changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {"module.b": {}}
+ }
+ });
+
+ // Check whether the changed file is in the destPath
+ const builtFileContent = await fs.readFile(`${destPath}/resources/b/module/dev/devTools.js`, {encoding: "utf8"});
+ t.true(builtFileContent.includes(`test("line added");`), "Build dest contains changed file content");
+
+
+ // Remove a source file in module.b
+ await fs.rm(`${fixtureTester.fixturePath}/dev/devTools.js`);
+
+ // #4 build (no cache, with changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {"module.b": {}}
+ }
+ });
+
+ // Check that the removed file is NOT in the destPath anymore
+ // (dist output should be totally empty: no source files -> no build result)
+ await t.throwsAsync(fs.readFile(`${destPath}/resources/b/module/dev/devTools.js`, {encoding: "utf8"}));
+
+
+ // #5 build (with cache, no changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // Add a new file in module.b
+ await fs.mkdir(`${fixtureTester.fixturePath}/dev/newFolder`, {recursive: true});
+ await fs.writeFile(`${fixtureTester.fixturePath}/dev/newFolder/newFile.js`,
+ `console.log("this is a new file which should be included in the build result")`);
+
+ // #6 build (no cache, with changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {"module.b": {}}
+ },
+ });
+
+ // Check whether the added file is in the destPath
+ const newFile = await fs.readFile(`${destPath}/resources/b/module/dev/newFolder/newFile.js`,
+ {encoding: "utf8"});
+ t.true(newFile.includes(`this is a new file which should be included in the build result`),
+ "Build dest contains correct file content");
+
+
+ // Add a new path mapping:
+ const originalUi5Yaml = await fs.readFile(`${fixtureTester.fixturePath}/ui5.yaml`, {encoding: "utf8"}); // for later
+ const newFileName = "someOtherNewFile.js";
+ const newFolderName = "newPathmapping";
+ const virtualPath = `/resources/b/module/${newFolderName}/`;
+ await fs.writeFile(`${fixtureTester.fixturePath}/ui5.yaml`,
+ `---
+specVersion: "5.0"
+type: module
+metadata:
+ name: module.b
+resources:
+ configuration:
+ paths:
+ /resources/b/module/dev/: dev
+ ${virtualPath}: ${newFolderName}`
+ );
+
+ // Create a resource for this new path mapping:
+ await fs.mkdir(`${fixtureTester.fixturePath}/${newFolderName}`, {recursive: true});
+ await fs.writeFile(`${fixtureTester.fixturePath}/${newFolderName}/${newFileName}`,
+ `console.log("this should be included in the build result if the path mapping has been set")`);
+
+ // #7 build (no cache, with changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {"module.b": {}}
+ },
+ });
+
+ // Check whether the added file is in the destPath
+ const someOtherNewFile = await fs.readFile(`${destPath}${virtualPath}${newFileName}`,
+ {encoding: "utf8"});
+ t.true(someOtherNewFile.includes(`path mapping has been set`), "Build dest contains correct file content");
+
+
+ // Remove the path mapping again (revert original ui5.yaml):
+ await fs.writeFile(`${fixtureTester.fixturePath}/ui5.yaml`, originalUi5Yaml);
+
+ // #8 build (with cache, with changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {} // -> cache can be reused
+ },
+ });
+
+ // Check that the added resource of the path mapping is NOT in the destPath anymore:
+ await t.throwsAsync(fs.readFile(`${destPath}${virtualPath}${newFileName}`,
+ {encoding: "utf8"}));
+
+
+ // #9 build (with cache, no changes, with dependencies)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ }
+ },
+ });
+});
diff --git a/packages/project/test/lib/build/ProjectBuilder.customTasks.integration.js b/packages/project/test/lib/build/ProjectBuilder.customTasks.integration.js
new file mode 100644
index 00000000000..1c060c8fa76
--- /dev/null
+++ b/packages/project/test/lib/build/ProjectBuilder.customTasks.integration.js
@@ -0,0 +1,438 @@
+import test from "ava";
+import fs from "node:fs/promises";
+import {createFixtureTesterFactory, registerBuildHooks} from "./__helper__/ProjectBuilderFixtureTester.js";
+
+const FixtureTester = createFixtureTesterFactory("customTasks");
+registerBuildHooks(test);
+
+test.serial("Build application.a (custom task and tag handling)", async (t) => {
+ const fixtureTester = new FixtureTester(t, "application.a");
+ const destPath = fixtureTester.destPath;
+
+ // #1 build (no cache, no changes, with custom tasks)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-customTask.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {}
+ }
+ }
+ });
+
+
+ // Create new file which should get tagged as "OmitFromBuildResult" by a custom task
+ await fs.writeFile(`${fixtureTester.fixturePath}/webapp/fileToBeOmitted.js`,
+ `console.log("this file should be omitted in the build result")`);
+
+ // #2 build (with cache, with changes, with custom tasks)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-customTask.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "application.a": {
+ skippedTasks: [
+ "escapeNonAsciiCharacters",
+ // Note: replaceCopyright is skipped because no copyright is configured in the project
+ "replaceCopyright",
+ "enhanceManifest",
+ "generateFlexChangesBundle",
+ "generateVersionInfo",
+ ]
+ }
+ }
+ }
+ });
+
+ // Check that fileToBeOmitted.js is not in dist
+ await t.throwsAsync(fs.readFile(`${destPath}/fileToBeOmitted.js`, {encoding: "utf8"}));
+
+
+ // #3 build (with cache, no changes, with custom tasks)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-customTask.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}
+ }
+ });
+
+ // Check that fileToBeOmitted.js is not in dist again
+ await t.throwsAsync(fs.readFile(`${destPath}/fileToBeOmitted.js`, {encoding: "utf8"}));
+
+
+ // Delete the file again
+ await fs.rm(`${fixtureTester.fixturePath}/webapp/fileToBeOmitted.js`);
+
+ // #4 build (with cache, with changes, with custom tasks)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-customTask.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {} // everything should be skipped (already done in very first build)
+ }
+ });
+});
+
+test.serial("Build application.a (multiple custom tasks)", async (t) => {
+ const fixtureTester = new FixtureTester(t, "application.a");
+ const destPath = fixtureTester.destPath;
+
+ // This test should cover a scenario with multiple custom tasks.
+ // Specifically, a tag is set in custom-task-1 on a resource which is read in custom-task-0 and custom-task-2.
+ // The expected behavior is that the tag is not present in custom-task-0 (which runs before custom-task-1),
+ // but is present in custom-task-2 (which runs after custom-task-1).
+ // (for testing purposes, the custom tasks already check for this tag by themselves and handle errors accordingly)
+
+ // #1 build (no cache, no changes, with custom tasks)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-multiple-customTasks.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {}
+ }
+ }
+ });
+
+ // #2 build (with cache, no changes, with custom tasks)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-multiple-customTasks.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // Create a new file to allow a new build:
+ // Logic of custom-task-1 will NOT handle this file, while custom-task-0 and 2 WILL DO it,
+ // resulting in custom-task-1 getting skipped (cache reuse).
+ // The test should then verify that the tag is still only readable for custom-task-2.
+ // This ensures that the build result is exactly the same with or without using the cache.
+ // (as in #1 build, the custom tasks already check for this tag by themselves and handle errors accordingly)
+ await fs.cp(`${fixtureTester.fixturePath}/webapp/test.js`,
+ `${fixtureTester.fixturePath}/webapp/test2.js`);
+
+ // #3 build (with cache, with changes, with custom tasks)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-multiple-customTasks.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "application.a": {
+ skippedTasks: [
+ "custom-task-1", // SHOULD BE SKIPPED
+ // remaining skipped tasks don't matter here:
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "generateFlexChangesBundle",
+ "replaceCopyright",
+ "generateVersionInfo",
+ ]
+ }
+ }
+ }
+ });
+});
+
+test.serial("Build application.a (multiple custom tasks 2)", async (t) => {
+ const fixtureTester = new FixtureTester(t, "application.a");
+ const destPath = fixtureTester.destPath;
+
+ // This test should cover a scenario with multiple custom tasks.
+
+ // #1 build (no cache, no changes, with custom tasks)
+ // During this build, "custom-task-0" sets the tag "isDebugVariant" to test.js.
+ // "custom-task-1" checks if it's able to read this tag.
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-multiple-customTasks-2.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {}
+ }
+ }
+ });
+
+
+ // Modify file to trigger a new build
+ // (this is related to the custom tasks):
+ await fs.appendFile(`${fixtureTester.fixturePath}/webapp/test.js`, `console.log("CHANGED FILE");`);
+
+ // #2 build (with cache, with changes, with custom tasks)
+ // During this build, "custom-task-0" sets a different tag to test.js (namely "OmitFromBuildResult").
+ // "custom-task-1" again checks if it's able to read this different tag.
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-multiple-customTasks-2.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "application.a": {
+ skippedTasks: [
+ "escapeNonAsciiCharacters",
+ "replaceCopyright",
+ "enhanceManifest",
+ "generateFlexChangesBundle",
+ "generateVersionInfo",
+ ]
+ }
+ }
+ }
+ });
+
+ // Check that test.js is omitted from build output:
+ await t.throwsAsync(fs.readFile(`${destPath}/test.js`, {encoding: "utf8"}));
+
+
+ // Add new file to trigger another build
+ // (this is unrelated to the custom tasks):
+ await fs.writeFile(`${fixtureTester.fixturePath}/webapp/newFile.js`, `console.log("NEW FILE");`);
+
+ // #4 build (with cache, with changes, with custom tasks)
+ // During this build, both custom tasks are expected to get skipped.
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-multiple-customTasks-2.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "application.a": {
+ skippedTasks: [
+ "custom-task-0",
+ "custom-task-1",
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "generateFlexChangesBundle",
+ "replaceCopyright",
+ "generateVersionInfo",
+ ]
+ }
+ }
+ }
+ });
+
+
+ // #5 build (with cache, no changes, with custom tasks)
+ // During this build, everything should get skipped.
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-multiple-customTasks-2.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}
+ }
+ });
+});
+
+// eslint-disable-next-line ava/no-skip-test
+test.serial.skip("Build application.a (dependency content changes)", async (t) => {
+ const fixtureTester = new FixtureTester(t, "application.a");
+ const destPath = fixtureTester.destPath;
+
+ // Scenario: A custom task reads dependency resources via taskUtil.getProject().getReader() and conditionally
+ // modifies application resources based on what it finds. When the dependency content changes, the application
+ // should be rebuilt so the custom task can react to the new dependency state.
+ //
+ // Currently skipped: The custom task accesses dependencies through taskUtil.getProject("library.d").getReader()
+ // rather than the monitored "dependencies" reader parameter. Reads through this path are not tracked by the
+ // caching system's ResourceRequestManager, so dependency changes don't invalidate the application's result cache.
+ // Fixing this requires tracking reads made via taskUtil.getProject().getReader() as dependency requests.
+
+ // #1 build (no cache, no changes, no dependencies)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-customTask-dependency-change.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "application.a": {}
+ }
+ }
+ });
+
+
+ // #2 build (with cache, no changes, no dependencies)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-customTask-dependency-change.yaml"},
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // Change content of library.d (this will not affect application.a):
+ const someJsOfLibrary = `${fixtureTester.fixturePath}/node_modules/library.d/main/src/library/d/some.js`;
+ await fs.appendFile(someJsOfLibrary, `\ntest("line added");\n`);
+
+ // #3 build (with cache, with changes, with dependencies)
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-customTask-dependency-change.yaml"},
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ }
+ }
+ });
+
+ // Check if library contains correct changed content:
+ const builtFileContent = await fs.readFile(`${destPath}/resources/library/d/some.js`, {encoding: "utf8"});
+ t.true(builtFileContent.includes(`test("line added");`), "Build dest contains changed file content");
+
+
+ // Change content of library.d again (this time it affects application.a):
+ await fs.writeFile(`${fixtureTester.fixturePath}/node_modules/library.d/main/src/library/d/newLibraryFile.js`,
+ `console.log("SOME NEW CONTENT");`);
+
+ // #4 build (no cache, with changes, with dependencies)
+ // This build should execute the custom task "task.dependency-change.js" again which now detects "newLibraryFile.js"
+ // and modifies a resource of application.a (namely "test.js").
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-customTask-dependency-change.yaml"},
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "application.a": {
+ skippedTasks: [
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "generateFlexChangesBundle",
+ "replaceCopyright",
+ ]
+ },
+ }
+ }
+ });
+
+ // Check that application.a contains correct changed content (test.js):
+ const builtFileContent2 = await fs.readFile(`${destPath}/test.js`, {encoding: "utf8"});
+ t.true(builtFileContent2.includes(`console.log('something new');`), "Build dest contains changed file content");
+});
+
+test.serial("Build application.a (cross-project tag change)", async (t) => {
+ const fixtureTester = new FixtureTester(t, "application.a");
+ const destPath = fixtureTester.destPath;
+ await fixtureTester._initialize();
+
+ // Modify library.d's ui5.yaml at runtime to add the dep-tag-setter custom task
+ const libraryDYamlPath = `${fixtureTester.fixturePath}/node_modules/library.d/ui5.yaml`;
+ await fs.writeFile(libraryDYamlPath,
+ `---
+specVersion: "2.3"
+type: library
+metadata:
+ name: library.d
+ copyright: Some fancy copyright
+resources:
+ configuration:
+ paths:
+ src: main/src
+ test: main/test
+builder:
+ customTasks:
+ - name: dep-tag-setter
+ afterTask: minify
+`);
+
+ // #1 build (no cache, with all dependencies)
+ // dep-tag-setter sets project:FirstBuild on some.js
+ // dep-tag-reader verifies project:FirstBuild is present
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-crossProject-tagChange.yaml"},
+ config: {
+ destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true},
+ excludedTasks: ["minify"],
+ },
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {}
+ }
+ }
+ });
+
+ // #2 build (cache, no changes) → all skipped
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-crossProject-tagChange.yaml"},
+ config: {
+ destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true},
+ excludedTasks: ["minify"],
+ },
+ assertions: {
+ projects: {}
+ }
+ });
+
+ // Change source in library.d to trigger rebuild
+ const someJsPath = `${fixtureTester.fixturePath}/node_modules/library.d/main/src/library/d/some.js`;
+ await fs.appendFile(someJsPath, `\nconsole.log("tag change trigger");\n`);
+
+ // #3 build (cache, library.d source changed)
+ // library.d rebuilt → dep-tag-setter now sets project:SubsequentBuild (different tag than #1)
+ // library.d's index signature changes due to the tag change
+ // application.a rebuilt because its dependency index changed
+ // dep-tag-reader verifies project:SubsequentBuild is present
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-crossProject-tagChange.yaml"},
+ config: {
+ destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true},
+ excludedTasks: ["minify"],
+ },
+ assertions: {
+ projects: {
+ "library.d": {
+ // FIXME: skippedTasks need empirical determination once runtime bugs are fixed
+ skippedTasks: [
+ "buildThemes",
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "replaceBuildtime",
+ ]
+ },
+ "application.a": {
+ // FIXME: skippedTasks need empirical determination once runtime bugs are fixed
+ skippedTasks: [
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "generateComponentPreload",
+ "generateFlexChangesBundle",
+ "replaceCopyright",
+ "replaceVersion",
+ "generateVersionInfo",
+ ]
+ },
+ }
+ }
+ });
+
+ // #4 build (cache, no changes) → all skipped
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-crossProject-tagChange.yaml"},
+ config: {
+ destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true},
+ excludedTasks: ["minify"],
+ },
+ assertions: {
+ projects: {}
+ }
+ });
+});
diff --git a/packages/project/test/lib/build/ProjectBuilder.dependencies.integration.js b/packages/project/test/lib/build/ProjectBuilder.dependencies.integration.js
new file mode 100644
index 00000000000..2f697d054e1
--- /dev/null
+++ b/packages/project/test/lib/build/ProjectBuilder.dependencies.integration.js
@@ -0,0 +1,530 @@
+import test from "ava";
+import fs from "node:fs/promises";
+import {createFixtureTesterFactory, registerBuildHooks} from "./__helper__/ProjectBuilderFixtureTester.js";
+
+const FixtureTester = createFixtureTesterFactory("deps");
+registerBuildHooks(test);
+
+test.serial("Build application.a (with various dependencies)", async (t) => {
+ const fixtureTester = new FixtureTester(t, "application.a");
+ const destPath = fixtureTester.destPath;
+
+ // #1 build (with empty cache)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: false, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {}
+ }
+ }
+ });
+
+
+ // #2 build (with cache, no changes, with dependencies)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // Add a "component" dependency to application.a:
+ await fixtureTester.addComponentDependency(`${fixtureTester.fixturePath}/webapp`);
+
+ // #3 build (no cache, with changes, with dependencies)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "component.z": {},
+ "application.a": {
+ skippedTasks: [
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "generateFlexChangesBundle",
+ "replaceCopyright",
+ "generateVersionInfo",
+ ]
+ }
+ }
+ }
+ });
+
+
+ // Add a "library" dependency to application.a:
+ await fixtureTester.addLibraryDependency(`${fixtureTester.fixturePath}/webapp`);
+
+ // #4 build (no cache, with changes, with dependencies)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "library.z": {},
+ "application.a": {
+ skippedTasks: [
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "generateFlexChangesBundle",
+ "replaceCopyright",
+ ]
+ }
+ }
+ }
+ });
+
+
+ // Add a "themelib" dependency to application.a:
+ await fixtureTester.addThemeLibraryDependency(`${fixtureTester.fixturePath}/webapp`);
+
+ // #5 build (no cache, with changes, with dependencies)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "themelib.z": {},
+ "application.a": {
+ skippedTasks: [
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "generateFlexChangesBundle",
+ "replaceCopyright",
+ "generateVersionInfo",
+ ]
+ }
+ }
+ }
+ });
+
+
+ // Add a "module" dependency to application.a:
+ await fixtureTester.addModuleDependency(`${fixtureTester.fixturePath}/webapp`);
+
+ // #6 build (no cache, with changes, with dependencies)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "module.z": {},
+ "application.a": {
+ skippedTasks: [
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "generateFlexChangesBundle",
+ "replaceCopyright",
+ "generateVersionInfo",
+ ]
+ }
+ }
+ }
+ });
+});
+
+test.serial("Build application.a (including only some dependencies)", async (t) => {
+ const fixtureTester = new FixtureTester(t, "application.a");
+ const destPath = fixtureTester.destPath;
+
+ // In this test, we're testing the "dependencyIncludes" build option
+ // which allows to include only a subset of the dependencies of a project in the build result.
+ // "application.a" has 4 dependencies defined: library.a, library.b, library.c and library.d.
+
+ // #1 build
+ // Only include library.a and library.b as dependencies, but not library.c and library.d:
+
+ // Note: For the initial build, ALL dependencies are built,
+ // due to the execution of the "generateVersionInfo" task in application.a which requires dependencies to be built.
+ // In subsequent builds, the cached results of the dependencies can be reused,
+ // so "includeDependency" can come to effect.
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: false,
+ dependencyIncludes: {includeDependency: ["library.a", "library.b"]}},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {}
+ }
+ }
+ });
+
+ // Check that only the included dependencies are in the destPath:
+ await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/a/library-preload.js`,
+ {encoding: "utf8"}));
+ await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/b/library-preload.js`,
+ {encoding: "utf8"}));
+
+ // Check that the remaining dependencies are NOT in the destPath:
+ // (Note: although library.c and library.d were built,
+ // they are not included in the build result because of the flag)
+ await t.throwsAsync(fs.readFile(`${destPath}/resources/library/c/library-preload.js`,
+ {encoding: "utf8"}));
+ await t.throwsAsync(fs.readFile(`${destPath}/resources/library/d/library-preload.js`,
+ {encoding: "utf8"}));
+
+
+ // #2 build
+ // Exclude library.d as dependency, but include all other dependencies
+ // (builds of library.a, library.b and library.c can be reused from cache):
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true,
+ dependencyIncludes: {includeAllDependencies: true, excludeDependency: ["library.d"]}},
+ assertions: {
+ projects: {}
+ }
+ });
+
+ // Check that only the included dependencies are in the destPath:
+ await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/a/library-preload.js`,
+ {encoding: "utf8"}));
+ await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/b/library-preload.js`,
+ {encoding: "utf8"}));
+ await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/c/library-preload.js`,
+ {encoding: "utf8"}));
+
+ // Check that the excluded dependency is NOT in the destPath:
+ await t.throwsAsync(fs.readFile(`${destPath}/resources/library/d/library-preload.js`,
+ {encoding: "utf8"}));
+
+
+ // #3 build
+ // Include all dependencies
+ // (builds of ALL libraries can be reused from cache):
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true,
+ dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {}
+ }
+ });
+
+ // Check that all dependencies are in the destPath:
+ await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/a/library-preload.js`,
+ {encoding: "utf8"}));
+ await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/b/library-preload.js`,
+ {encoding: "utf8"}));
+ await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/c/library-preload.js`,
+ {encoding: "utf8"}));
+ await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/d/library-preload.js`,
+ {encoding: "utf8"}));
+
+
+ // Delete a dependency ("library.d") from application.a:
+ await fs.rm(`${fixtureTester.fixturePath}/node_modules/library.d`, {recursive: true, force: true});
+ const packageJsonContent = JSON.parse(
+ await fs.readFile(`${fixtureTester.fixturePath}/package.json`, {encoding: "utf8"}));
+ delete packageJsonContent.dependencies["library.d"];
+ await fs.writeFile(`${fixtureTester.fixturePath}/package.json`, JSON.stringify(packageJsonContent, null, 2));
+
+ // #4 build
+ // Build application.a again with "includeAllDependencies"
+ // and check with assertion "allProjects" that "library.d" isn't even seen:
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true,
+ dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ allProjects: ["library.a", "library.b", "library.c", "application.a"],
+ projects: {
+ "application.a": {
+ skippedTasks: [
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "generateComponentPreload",
+ "generateFlexChangesBundle",
+ "minify",
+ "replaceCopyright",
+ "replaceVersion",
+ ]
+ }
+ },
+ }
+ });
+});
+
+test.serial("Build library.d (with various dependencies)", async (t) => {
+ const fixtureTester = new FixtureTester(t, "library.d");
+ const destPath = fixtureTester.destPath;
+
+ // #1 build (with empty cache)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: false},
+ assertions: {
+ projects: {"library.d": {}}
+ }
+ });
+
+
+ // #2 build (with cache, no changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // Add a "library" dependency to library.d:
+ await fixtureTester.addLibraryDependency(`${fixtureTester.fixturePath}/main/src/library/d`);
+
+ // #3 build (no cache, with changes, with dependencies)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "library.z": {},
+ "library.d": {
+ skippedTasks: [
+ "buildThemes",
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "replaceBuildtime",
+ ]
+ },
+ }
+ }
+ });
+
+
+ // Add a "themelib" dependency to library.d:
+ await fixtureTester.addThemeLibraryDependency(`${fixtureTester.fixturePath}/main/src/library/d`);
+
+ // #4 build (no cache, with changes, with dependencies)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "themelib.z": {},
+ "library.d": {
+ skippedTasks: [
+ "buildThemes",
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "replaceBuildtime",
+ ]
+ }
+ }
+ }
+ });
+});
+
+test.serial("Build theme.library.e (with various dependencies)", async (t) => {
+ const fixtureTester = new FixtureTester(t, "theme.library.e");
+ const destPath = fixtureTester.destPath;
+
+ // #1 build (with empty cache)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: false, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {"theme.library.e": {}}
+ }
+ });
+
+
+ // #2 build (with cache, no changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // Add a "library" dependency to theme.library.e:
+ await fixtureTester.addLibraryDependency(`${fixtureTester.fixturePath}/src/theme/library/e/themes/my_theme`);
+
+ // #3 build (no cache, with changes, with dependencies)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "library.z": {},
+ "theme.library.e": {
+ skippedTasks: [
+ "buildThemes",
+ "replaceCopyright",
+ "replaceVersion",
+ ]
+ },
+ }
+ }
+ });
+});
+
+test.serial("Build component.a (with various dependencies)", async (t) => {
+ const fixtureTester = new FixtureTester(t, "component.a");
+ const destPath = fixtureTester.destPath;
+
+ // #1 build (with empty cache)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: false, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "component.a": {}
+ }
+ }
+ });
+
+
+ // #2 build (with cache, no changes, with dependencies)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // Add a "component" dependency to component.a:
+ await fixtureTester.addComponentDependency(`${fixtureTester.fixturePath}/src`);
+
+ // #3 build (no cache, with changes, with dependencies)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "component.z": {},
+ "component.a": {
+ skippedTasks: [
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "generateFlexChangesBundle",
+ "replaceCopyright",
+ ]
+ }
+ }
+ }
+ });
+
+
+ // Add a "library" dependency to component.a:
+ await fixtureTester.addLibraryDependency(`${fixtureTester.fixturePath}/src`);
+
+ // #4 build (no cache, with changes, with dependencies)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "library.z": {},
+ "component.a": {
+ skippedTasks: [
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "generateFlexChangesBundle",
+ "replaceCopyright",
+ ]
+ }
+ }
+ }
+ });
+
+
+ // Add a "themelib" dependency to component.a:
+ await fixtureTester.addThemeLibraryDependency(`${fixtureTester.fixturePath}/src`);
+
+ // #5 build (no cache, with changes, with dependencies)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "themelib.z": {},
+ "component.a": {
+ skippedTasks: [
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "generateFlexChangesBundle",
+ "replaceCopyright",
+ ]
+ }
+ }
+ }
+ });
+
+
+ // Add a "module" dependency to component.a:
+ await fixtureTester.addModuleDependency(`${fixtureTester.fixturePath}/src`);
+
+ // #6 build (no cache, with changes, with dependencies)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "module.z": {},
+ "component.a": {
+ skippedTasks: [
+ "enhanceManifest",
+ "escapeNonAsciiCharacters",
+ "generateFlexChangesBundle",
+ "replaceCopyright",
+ ]
+ }
+ }
+ }
+ });
+});
+
+test.serial("Build module.b (with various dependencies)", async (t) => {
+ const fixtureTester = new FixtureTester(t, "module.b");
+ const destPath = fixtureTester.destPath;
+
+ // #1 build (no cache, no changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "module.b": {}
+ }
+ },
+ });
+
+
+ // #2 build (with cache, no changes)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {}
+ }
+ });
+
+
+ // Add a "library" dependency to module.b:
+ await fixtureTester.addLibraryDependency(`${fixtureTester.fixturePath}/dev`);
+
+ // #3 build (no cache, with changes, with dependencies)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "library.z": {},
+ "module.b": {}
+ }
+ },
+ });
+
+
+ // Add a "themelib" dependency to module.b:
+ await fixtureTester.addThemeLibraryDependency(`${fixtureTester.fixturePath}/dev`);
+
+ // #4 build (no cache, with changes, with dependencies)
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "themelib.z": {},
+ "module.b": {}
+ }
+ }
+ });
+});
diff --git a/packages/project/test/lib/build/ProjectBuilder.integration.js b/packages/project/test/lib/build/ProjectBuilder.integration.js
deleted file mode 100644
index b4c2e5255d6..00000000000
--- a/packages/project/test/lib/build/ProjectBuilder.integration.js
+++ /dev/null
@@ -1,3166 +0,0 @@
-import test from "ava";
-import sinonGlobal from "sinon";
-import {fileURLToPath} from "node:url";
-import fs from "node:fs/promises";
-import {graphFromPackageDependencies} from "../../../lib/graph/graph.js";
-import {setLogLevel} from "@ui5/logger";
-import Cache from "../../../lib/build/cache/Cache.js";
-
-// Ensures that all logging code paths are tested
-setLogLevel("silly");
-
-test.beforeEach((t) => {
- const sinon = t.context.sinon = sinonGlobal.createSandbox();
-
- t.context.logEventStub = sinon.stub();
- t.context.buildMetadataEventStub = sinon.stub();
- t.context.projectBuildMetadataEventStub = sinon.stub();
- t.context.buildStatusEventStub = sinon.stub();
- t.context.projectBuildStatusEventStub = sinon.stub();
-
- process.on("ui5.log", t.context.logEventStub);
- process.on("ui5.build-metadata", t.context.buildMetadataEventStub);
- process.on("ui5.project-build-metadata", t.context.projectBuildMetadataEventStub);
- process.on("ui5.build-status", t.context.buildStatusEventStub);
- process.on("ui5.project-build-status", t.context.projectBuildStatusEventStub);
-});
-
-test.afterEach.always((t) => {
- t.context.sinon.restore();
-
- process.off("ui5.log", t.context.logEventStub);
- process.off("ui5.build-metadata", t.context.buildMetadataEventStub);
- process.off("ui5.project-build-metadata", t.context.projectBuildMetadataEventStub);
- process.off("ui5.build-status", t.context.buildStatusEventStub);
- process.off("ui5.project-build-status", t.context.projectBuildStatusEventStub);
-});
-
-test.serial("Build application.a project multiple times", async (t) => {
- const fixtureTester = new FixtureTester(t, "application.a");
- const destPath = fixtureTester.destPath;
-
- // #1 build (with empty cache)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: false},
- assertions: {
- projects: {
- // Default builds of project type "application" include the "generateVersionInfo"
- // task which requires dependencies to be built, so all dependencies are expected to be built here.
- // Subsequent builds can reuse the cached results of these dependencies.
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {}
- }
- }
- });
-
-
- // #2 build (with cache, no changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}
- }
- });
-
-
- // Change a source file in application.a
- const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`;
- await fs.appendFile(changedFilePath, `\ntest("line added");\n`);
-
- // #3 build (with cache, with changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "application.a": {
- skippedTasks: [
- "escapeNonAsciiCharacters",
- // Note: replaceCopyright is skipped because no copyright is configured in the project
- "replaceCopyright",
- "enhanceManifest",
- "generateFlexChangesBundle",
- "generateVersionInfo",
- ]
- }
- }
- }
- });
-
- // Check whether the changed file is in the destPath
- const builtFileContent = await fs.readFile(`${destPath}/test.js`, {encoding: "utf8"});
- t.true(builtFileContent.includes(`test("line added");`), "Build dest contains changed file content");
-
-
- // #4 build (with cache, no changes, with dependencies)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- // Dependencies are NOT rebuilt because
- // they were already built in build #1 and can be reused from cache.
- // Thus, empty assertion for built projects.
- projects: {}
- }
- });
-
-
- // #5 build (with cache, no changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}
- }
- });
-
-
- // #6 build (with cache, no changes, with dependencies)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {}
- }
- });
-
-
- // #7 build (with cache, no changes, with custom tasks)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-customTask.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "application.a": {}
- }
- }
- });
-
-
- // #8 build (with cache, no changes, with custom tasks)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-customTask.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}
- }
- });
-
-
- // #9 build (with cache, no changes, with dependencies)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {}
- }
- });
-
-
- // Change a source file with existing source map in application.a
- const fileWithSourceMapPath =
- `${fixtureTester.fixturePath}/webapp/thirdparty/scriptWithSourceMap.js`;
- const fileWithSourceMapContent = await fs.readFile(fileWithSourceMapPath, {encoding: "utf8"});
- await fs.writeFile(
- fileWithSourceMapPath,
- fileWithSourceMapContent.replace(
- `This is a script with a source map.`,
- `This is a CHANGED script with a source map.`
- )
- );
- const sourceMapPath = `${fixtureTester.fixturePath}/webapp/thirdparty/scriptWithSourceMap.js.map`;
- const sourceMapContent = await fs.readFile(sourceMapPath, {encoding: "utf8"});
- await fs.writeFile(
- sourceMapPath,
- sourceMapContent.replace(
- `This is a script with a source map.`,
- `This is a CHANGED script with a source map.`
- )
- );
-
- // #10 build (with cache, with changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "application.a": {
- skippedTasks: [
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "generateFlexChangesBundle",
- "generateVersionInfo",
- "replaceCopyright"
- ]
- }
- }
- }
- });
-
-
- // Add a new file to application.a
- await fs.writeFile(`${fixtureTester.fixturePath}/webapp/someNew.js`,
- `console.log("SOME NEW CONTENT");\n`
- );
-
- // #11 build (with cache, with changes - someNew.js added)
- // Tasks that don't depend on someNew.js can reuse their caches from build #10.
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {"application.a": {
- skippedTasks: [
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "generateFlexChangesBundle",
- "replaceCopyright",
- "generateVersionInfo",
- ]
- }}
- }
- });
-
- await fs.rm(`${fixtureTester.fixturePath}/webapp/someNew.js`);
-
- // #12 build (with cache, with changes - someNew.js removed)
- // Source state matches build #10's cached result -> cache reused, everything skipped
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {},
- }
- });
-});
-
-test.serial("Build application.a (with various dependencies)", async (t) => {
- const fixtureTester = new FixtureTester(t, "application.a");
- const destPath = fixtureTester.destPath;
-
- // #1 build (with empty cache)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: false, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {}
- }
- }
- });
-
-
- // #2 build (with cache, no changes, with dependencies)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {}
- }
- });
-
-
- // Add a "component" dependency to application.a:
- await fixtureTester.addComponentDependency(`${fixtureTester.fixturePath}/webapp`);
-
- // #3 build (no cache, with changes, with dependencies)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "component.z": {},
- "application.a": {
- skippedTasks: [
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "generateFlexChangesBundle",
- "replaceCopyright",
- "generateVersionInfo",
- ]
- }
- }
- }
- });
-
-
- // Add a "library" dependency to application.a:
- await fixtureTester.addLibraryDependency(`${fixtureTester.fixturePath}/webapp`);
-
- // #4 build (no cache, with changes, with dependencies)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "library.z": {},
- "application.a": {
- skippedTasks: [
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "generateFlexChangesBundle",
- "replaceCopyright",
- ]
- }
- }
- }
- });
-
-
- // Add a "themelib" dependency to application.a:
- await fixtureTester.addThemeLibraryDependency(`${fixtureTester.fixturePath}/webapp`);
-
- // #5 build (no cache, with changes, with dependencies)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "themelib.z": {},
- "application.a": {
- skippedTasks: [
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "generateFlexChangesBundle",
- "replaceCopyright",
- "generateVersionInfo",
- ]
- }
- }
- }
- });
-
-
- // Add a "module" dependency to application.a:
- await fixtureTester.addModuleDependency(`${fixtureTester.fixturePath}/webapp`);
-
- // #6 build (no cache, with changes, with dependencies)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "module.z": {},
- "application.a": {
- skippedTasks: [
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "generateFlexChangesBundle",
- "replaceCopyright",
- "generateVersionInfo",
- ]
- }
- }
- }
- });
-});
-
-test.serial("Build application.a (including only some dependencies)", async (t) => {
- const fixtureTester = new FixtureTester(t, "application.a");
- const destPath = fixtureTester.destPath;
-
- // In this test, we're testing the "dependencyIncludes" build option
- // which allows to include only a subset of the dependencies of a project in the build result.
- // "application.a" has 4 dependencies defined: library.a, library.b, library.c and library.d.
-
- // #1 build
- // Only include library.a and library.b as dependencies, but not library.c and library.d:
-
- // Note: For the initial build, ALL dependencies are built,
- // due to the execution of the "generateVersionInfo" task in application.a which requires dependencies to be built.
- // In subsequent builds, the cached results of the dependencies can be reused,
- // so "includeDependency" can come to effect.
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: false,
- dependencyIncludes: {includeDependency: ["library.a", "library.b"]}},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {}
- }
- }
- });
-
- // Check that only the included dependencies are in the destPath:
- await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/a/library-preload.js`,
- {encoding: "utf8"}));
- await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/b/library-preload.js`,
- {encoding: "utf8"}));
-
- // Check that the remaining dependencies are NOT in the destPath:
- // (Note: although library.c and library.d were built,
- // they are not included in the build result because of the flag)
- await t.throwsAsync(fs.readFile(`${destPath}/resources/library/c/library-preload.js`,
- {encoding: "utf8"}));
- await t.throwsAsync(fs.readFile(`${destPath}/resources/library/d/library-preload.js`,
- {encoding: "utf8"}));
-
-
- // #2 build
- // Exclude library.d as dependency, but include all other dependencies
- // (builds of library.a, library.b and library.c can be reused from cache):
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true,
- dependencyIncludes: {includeAllDependencies: true, excludeDependency: ["library.d"]}},
- assertions: {
- projects: {}
- }
- });
-
- // Check that only the included dependencies are in the destPath:
- await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/a/library-preload.js`,
- {encoding: "utf8"}));
- await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/b/library-preload.js`,
- {encoding: "utf8"}));
- await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/c/library-preload.js`,
- {encoding: "utf8"}));
-
- // Check that the excluded dependency is NOT in the destPath:
- await t.throwsAsync(fs.readFile(`${destPath}/resources/library/d/library-preload.js`,
- {encoding: "utf8"}));
-
-
- // #3 build
- // Include all dependencies
- // (builds of ALL libraries can be reused from cache):
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true,
- dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {}
- }
- });
-
- // Check that all dependencies are in the destPath:
- await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/a/library-preload.js`,
- {encoding: "utf8"}));
- await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/b/library-preload.js`,
- {encoding: "utf8"}));
- await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/c/library-preload.js`,
- {encoding: "utf8"}));
- await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/d/library-preload.js`,
- {encoding: "utf8"}));
-
-
- // Delete a dependency ("library.d") from application.a:
- await fs.rm(`${fixtureTester.fixturePath}/node_modules/library.d`, {recursive: true, force: true});
- const packageJsonContent = JSON.parse(
- await fs.readFile(`${fixtureTester.fixturePath}/package.json`, {encoding: "utf8"}));
- delete packageJsonContent.dependencies["library.d"];
- await fs.writeFile(`${fixtureTester.fixturePath}/package.json`, JSON.stringify(packageJsonContent, null, 2));
-
- // #4 build
- // Build application.a again with "includeAllDependencies"
- // and check with assertion "allProjects" that "library.d" isn't even seen:
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true,
- dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- allProjects: ["library.a", "library.b", "library.c", "application.a"],
- projects: {
- "application.a": {
- skippedTasks: [
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "generateComponentPreload",
- "generateFlexChangesBundle",
- "minify",
- "replaceCopyright",
- "replaceVersion",
- ]
- }
- },
- }
- });
-});
-
-test.serial("Build application.a (custom task and tag handling)", async (t) => {
- const fixtureTester = new FixtureTester(t, "application.a");
- const destPath = fixtureTester.destPath;
-
- // #1 build (no cache, no changes, with custom tasks)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-customTask.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {}
- }
- }
- });
-
-
- // Create new file which should get tagged as "OmitFromBuildResult" by a custom task
- await fs.writeFile(`${fixtureTester.fixturePath}/webapp/fileToBeOmitted.js`,
- `console.log("this file should be omitted in the build result")`);
-
- // #2 build (with cache, with changes, with custom tasks)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-customTask.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "application.a": {
- skippedTasks: [
- "escapeNonAsciiCharacters",
- // Note: replaceCopyright is skipped because no copyright is configured in the project
- "replaceCopyright",
- "enhanceManifest",
- "generateFlexChangesBundle",
- "generateVersionInfo",
- ]
- }
- }
- }
- });
-
- // Check that fileToBeOmitted.js is not in dist
- await t.throwsAsync(fs.readFile(`${destPath}/fileToBeOmitted.js`, {encoding: "utf8"}));
-
-
- // #3 build (with cache, no changes, with custom tasks)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-customTask.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}
- }
- });
-
- // Check that fileToBeOmitted.js is not in dist again
- await t.throwsAsync(fs.readFile(`${destPath}/fileToBeOmitted.js`, {encoding: "utf8"}));
-
-
- // Delete the file again
- await fs.rm(`${fixtureTester.fixturePath}/webapp/fileToBeOmitted.js`);
-
- // #4 build (with cache, with changes, with custom tasks)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-customTask.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {} // everything should be skipped (already done in very first build)
- }
- });
-});
-
-test.serial("Build application.a (multiple custom tasks)", async (t) => {
- const fixtureTester = new FixtureTester(t, "application.a");
- const destPath = fixtureTester.destPath;
-
- // This test should cover a scenario with multiple custom tasks.
- // Specifically, a tag is set in custom-task-1 on a resource which is read in custom-task-0 and custom-task-2.
- // The expected behavior is that the tag is not present in custom-task-0 (which runs before custom-task-1),
- // but is present in custom-task-2 (which runs after custom-task-1).
- // (for testing purposes, the custom tasks already check for this tag by themselves and handle errors accordingly)
-
- // #1 build (no cache, no changes, with custom tasks)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-multiple-customTasks.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {}
- }
- }
- });
-
- // #2 build (with cache, no changes, with custom tasks)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-multiple-customTasks.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}
- }
- });
-
-
- // Create a new file to allow a new build:
- // Logic of custom-task-1 will NOT handle this file, while custom-task-0 and 2 WILL DO it,
- // resulting in custom-task-1 getting skipped (cache reuse).
- // The test should then verify that the tag is still only readable for custom-task-2.
- // This ensures that the build result is exactly the same with or without using the cache.
- // (as in #1 build, the custom tasks already check for this tag by themselves and handle errors accordingly)
- await fs.cp(`${fixtureTester.fixturePath}/webapp/test.js`,
- `${fixtureTester.fixturePath}/webapp/test2.js`);
-
- // #3 build (with cache, with changes, with custom tasks)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-multiple-customTasks.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "application.a": {
- skippedTasks: [
- "custom-task-1", // SHOULD BE SKIPPED
- // remaining skipped tasks don't matter here:
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "generateFlexChangesBundle",
- "replaceCopyright",
- "generateVersionInfo",
- ]
- }
- }
- }
- });
-});
-
-test.serial("Build application.a (multiple custom tasks 2)", async (t) => {
- const fixtureTester = new FixtureTester(t, "application.a");
- const destPath = fixtureTester.destPath;
-
- // This test should cover a scenario with multiple custom tasks.
-
- // #1 build (no cache, no changes, with custom tasks)
- // During this build, "custom-task-0" sets the tag "isDebugVariant" to test.js.
- // "custom-task-1" checks if it's able to read this tag.
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-multiple-customTasks-2.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {}
- }
- }
- });
-
-
- // Modify file to trigger a new build
- // (this is related to the custom tasks):
- await fs.appendFile(`${fixtureTester.fixturePath}/webapp/test.js`, `console.log("CHANGED FILE");`);
-
- // #2 build (with cache, with changes, with custom tasks)
- // During this build, "custom-task-0" sets a different tag to test.js (namely "OmitFromBuildResult").
- // "custom-task-1" again checks if it's able to read this different tag.
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-multiple-customTasks-2.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "application.a": {
- skippedTasks: [
- "escapeNonAsciiCharacters",
- "replaceCopyright",
- "enhanceManifest",
- "generateFlexChangesBundle",
- "generateVersionInfo",
- ]
- }
- }
- }
- });
-
- // Check that test.js is omitted from build output:
- await t.throwsAsync(fs.readFile(`${destPath}/test.js`, {encoding: "utf8"}));
-
-
- // Add new file to trigger another build
- // (this is unrelated to the custom tasks):
- await fs.writeFile(`${fixtureTester.fixturePath}/webapp/newFile.js`, `console.log("NEW FILE");`);
-
- // #4 build (with cache, with changes, with custom tasks)
- // During this build, both custom tasks are expected to get skipped.
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-multiple-customTasks-2.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "application.a": {
- skippedTasks: [
- "custom-task-0",
- "custom-task-1",
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "generateFlexChangesBundle",
- "replaceCopyright",
- "generateVersionInfo",
- ]
- }
- }
- }
- });
-
-
- // #5 build (with cache, no changes, with custom tasks)
- // During this build, everything should get skipped.
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-multiple-customTasks-2.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}
- }
- });
-});
-
-// eslint-disable-next-line ava/no-skip-test
-test.serial.skip("Build application.a (dependency content changes)", async (t) => {
- const fixtureTester = new FixtureTester(t, "application.a");
- const destPath = fixtureTester.destPath;
-
- // Scenario: A custom task reads dependency resources via taskUtil.getProject().getReader() and conditionally
- // modifies application resources based on what it finds. When the dependency content changes, the application
- // should be rebuilt so the custom task can react to the new dependency state.
- //
- // Currently skipped: The custom task accesses dependencies through taskUtil.getProject("library.d").getReader()
- // rather than the monitored "dependencies" reader parameter. Reads through this path are not tracked by the
- // caching system's ResourceRequestManager, so dependency changes don't invalidate the application's result cache.
- // Fixing this requires tracking reads made via taskUtil.getProject().getReader() as dependency requests.
-
- // #1 build (no cache, no changes, no dependencies)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-customTask-dependency-change.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "application.a": {}
- }
- }
- });
-
-
- // #2 build (with cache, no changes, no dependencies)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-customTask-dependency-change.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}
- }
- });
-
-
- // Change content of library.d (this will not affect application.a):
- const someJsOfLibrary = `${fixtureTester.fixturePath}/node_modules/library.d/main/src/library/d/some.js`;
- await fs.appendFile(someJsOfLibrary, `\ntest("line added");\n`);
-
- // #3 build (with cache, with changes, with dependencies)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-customTask-dependency-change.yaml"},
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- }
- }
- });
-
- // Check if library contains correct changed content:
- const builtFileContent = await fs.readFile(`${destPath}/resources/library/d/some.js`, {encoding: "utf8"});
- t.true(builtFileContent.includes(`test("line added");`), "Build dest contains changed file content");
-
-
- // Change content of library.d again (this time it affects application.a):
- await fs.writeFile(`${fixtureTester.fixturePath}/node_modules/library.d/main/src/library/d/newLibraryFile.js`,
- `console.log("SOME NEW CONTENT");`);
-
- // #4 build (no cache, with changes, with dependencies)
- // This build should execute the custom task "task.dependency-change.js" again which now detects "newLibraryFile.js"
- // and modifies a resource of application.a (namely "test.js").
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-customTask-dependency-change.yaml"},
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "library.d": {},
- "application.a": {
- skippedTasks: [
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "generateFlexChangesBundle",
- "replaceCopyright",
- ]
- },
- }
- }
- });
-
- // Check that application.a contains correct changed content (test.js):
- const builtFileContent2 = await fs.readFile(`${destPath}/test.js`, {encoding: "utf8"});
- t.true(builtFileContent2.includes(`console.log('something new');`), "Build dest contains changed file content");
-});
-
-test.serial("Build application.a (cross-project tag change)", async (t) => {
- const fixtureTester = new FixtureTester(t, "application.a");
- const destPath = fixtureTester.destPath;
- await fixtureTester._initialize();
-
- // Modify library.d's ui5.yaml at runtime to add the dep-tag-setter custom task
- const libraryDYamlPath = `${fixtureTester.fixturePath}/node_modules/library.d/ui5.yaml`;
- await fs.writeFile(libraryDYamlPath,
- `---
-specVersion: "2.3"
-type: library
-metadata:
- name: library.d
- copyright: Some fancy copyright
-resources:
- configuration:
- paths:
- src: main/src
- test: main/test
-builder:
- customTasks:
- - name: dep-tag-setter
- afterTask: minify
-`);
-
- // #1 build (no cache, with all dependencies)
- // dep-tag-setter sets project:FirstBuild on some.js
- // dep-tag-reader verifies project:FirstBuild is present
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-crossProject-tagChange.yaml"},
- config: {
- destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true},
- excludedTasks: ["minify"],
- },
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {}
- }
- }
- });
-
- // #2 build (cache, no changes) → all skipped
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-crossProject-tagChange.yaml"},
- config: {
- destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true},
- excludedTasks: ["minify"],
- },
- assertions: {
- projects: {}
- }
- });
-
- // Change source in library.d to trigger rebuild
- const someJsPath = `${fixtureTester.fixturePath}/node_modules/library.d/main/src/library/d/some.js`;
- await fs.appendFile(someJsPath, `\nconsole.log("tag change trigger");\n`);
-
- // #3 build (cache, library.d source changed)
- // library.d rebuilt → dep-tag-setter now sets project:SubsequentBuild (different tag than #1)
- // library.d's index signature changes due to the tag change
- // application.a rebuilt because its dependency index changed
- // dep-tag-reader verifies project:SubsequentBuild is present
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-crossProject-tagChange.yaml"},
- config: {
- destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true},
- excludedTasks: ["minify"],
- },
- assertions: {
- projects: {
- "library.d": {
- // FIXME: skippedTasks need empirical determination once runtime bugs are fixed
- skippedTasks: [
- "buildThemes",
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "replaceBuildtime",
- ]
- },
- "application.a": {
- // FIXME: skippedTasks need empirical determination once runtime bugs are fixed
- skippedTasks: [
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "generateComponentPreload",
- "generateFlexChangesBundle",
- "replaceCopyright",
- "replaceVersion",
- "generateVersionInfo",
- ]
- },
- }
- }
- });
-
- // #4 build (cache, no changes) → all skipped
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-crossProject-tagChange.yaml"},
- config: {
- destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true},
- excludedTasks: ["minify"],
- },
- assertions: {
- projects: {}
- }
- });
-});
-
-test.serial("Build application.a (JSDoc build)", async (t) => {
- const fixtureTester = new FixtureTester(t, "application.a");
- const destPath = fixtureTester.destPath;
-
- // This test should cover a scenario with an application depending on a library.
- // We're executing a JSDoc build including dependencies (as with "ui5 build jsdoc --all")
- // and testing if the output contains the expected JSDoc contents.
- // Then, we're adding some additional JSDoc annotations to the library
- // and testing the same again.
-
- // #1 build (no cache, no changes)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5.yaml"},
- config: {destPath, cleanDest: true, jsdoc: "jsdoc", dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {}
- }
- }
- });
-
- // Check that JSDoc build ran successfully:
- await t.throwsAsync(fs.readFile(`${destPath}/resources/library/d/some-dbg.js`, {encoding: "utf8"}));
- await t.throwsAsync(fs.readFile(`${destPath}/resources/library/d/some.js.map`, {encoding: "utf8"}));
- const builtFileContent = await fs.readFile(`${destPath}/resources/library/d/some.js`, {encoding: "utf8"});
- // Check that output contains correct file content:
- t.false(builtFileContent.includes(`//# sourceMappingURL=some.js.map`),
- "Build dest does not contain source map reference");
-
-
- // #2 build (with cache, no changes)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5.yaml"},
- config: {destPath, cleanDest: true, jsdoc: "jsdoc", dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {}
- }
- });
-
-
- // Add additional JSDoc annotations to library.d:
- const jsdocContent = `/*!
-* ` + "${copyright}" + `
-*/
-
-/**
-* Example JSDoc annotation
-*
-* @public
-* @static
-* @param {object} param
-* @returns {string} output
-*/
-function functionWithJSDoc(param) {return "test"}`;
-
- await fs.writeFile(`${fixtureTester.fixturePath}/node_modules/library.d/main/src/library/d/some.js`,
- jsdocContent);
-
- // #3 build (no cache, with changes)
- // application.a should get skipped:
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5.yaml"},
- config: {destPath, cleanDest: true, jsdoc: "jsdoc", dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "library.d": {
- skippedTasks: [
- "buildThemes",
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "executeJsdocSdkTransformation",
- ]
- }
- }
- }
- });
-
- // Check that JSDoc build ran successfully:
- await t.throwsAsync(fs.readFile(`${destPath}/resources/library/d/some-dbg.js`, {encoding: "utf8"}));
- await t.throwsAsync(fs.readFile(`${destPath}/resources/library/d/some.js.map`, {encoding: "utf8"}));
- const builtFileContent2 = await fs.readFile(`${destPath}/resources/library/d/some.js`, {encoding: "utf8"});
- t.true(builtFileContent2.includes(`Example JSDoc annotation`), "Build dest contains new JSDoc content");
- // Check that output contains new file content:
- t.false(builtFileContent2.includes(`//# sourceMappingURL=some.js.map`),
- "Build dest does not contain source map reference");
-
-
- // #4 build (no cache, no changes)
- // Normal build again (non-JSDoc build); should not execute task "generateJsdoc":
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5.yaml"},
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {}
- }
- }
- });
-
- // Check that normal build ran successfully:
- await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/d/some-dbg.js`, {encoding: "utf8"}));
- await t.notThrowsAsync(fs.readFile(`${destPath}/resources/library/d/some.js.map`, {encoding: "utf8"}));
- const builtFileContent3 = await fs.readFile(`${destPath}/resources/library/d/some.js`, {encoding: "utf8"});
- t.false(builtFileContent3.includes(`Example JSDoc annotation`), "Build dest doesn't contain JSDoc content anymore");
- // Check that output contains content generated by the normal build:
- t.true(builtFileContent3.includes(`//# sourceMappingURL=some.js.map`),
- "Build dest does contain source map reference");
-});
-
-test.serial("Build application.a (Custom Component preload configuration)", async (t) => {
- const fixtureTester = new FixtureTester(t, "application.a");
- const destPath = fixtureTester.destPath;
-
- // In this test, we're testing the behavior of a custom preload configuration
- // which is defined in "ui5-custom-preload-config.yaml".
- // This custom preload configuration generates a Component-preload.js similar to a default one.
- // However, it will omit a resource ("scriptWithSourceMap.js") from the bundle.
-
- // #1 build (no cache, no changes)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-custom-preload-config.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {}
- }
- }
- });
-
- // Check that generated preload bundle doesn't contain the omitted file:
- t.false((await fs.readFile(`${destPath}/Component-preload.js`, {encoding: "utf8"}))
- .includes("id1/thirdparty/scriptWithSourceMap.js"));
-
-
- // #2 build (with cache, no changes)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-custom-preload-config.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}
- }
- });
-});
-
-test.serial("Build application.a (self-contained build)", async (t) => {
- const fixtureTester = new FixtureTester(t, "application.a");
- const destPath = fixtureTester.destPath;
-
- // We're executing a self-contained build including dependencies (as with "ui5 build self-contained --all")
- // and testing if the output contains the expected self-contained bundle.
- // Then, we're changing the content only of application.a
- // and testing if the self-contained build output changes accordingly.
-
- // #1 build (no cache, no changes)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5.yaml"},
- config: {destPath, cleanDest: true, selfContained: "self-contained",
- dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {}
- }
- }
- });
-
- // Check that output contains the correct content:
- const builtFileContent = await fs.readFile(`${destPath}/resources/sap-ui-custom.js`, {encoding: "utf8"});
- t.true(builtFileContent.includes(`"id1/test.js":'function test(t){var o=t;console.log(o)}test();\\n'`));
-
-
- // #2 build (with cache, no changes)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5.yaml"},
- config: {destPath, cleanDest: true, selfContained: "self-contained",
- dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {}
- }
- });
-
-
- // Remove the file "test.js" from application.a:
- await fs.rm(`${fixtureTester.fixturePath}/webapp/test.js`);
-
- // #3 build (with cache, with changes)
- // Dependencies should get skipped, application.a should get rebuilt:
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5.yaml"},
- config: {destPath, cleanDest: true, selfContained: "self-contained",
- dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "application.a": {
- skippedTasks: [
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "generateFlexChangesBundle",
- "replaceCopyright",
- "transformBootstrapHtml",
- "generateVersionInfo",
- ]
- }
- }
- }
- });
-
- // Check that output contains the correct content (test.js should be missing):
- const builtFileContent2 = await fs.readFile(`${destPath}/resources/sap-ui-custom.js`, {encoding: "utf8"});
- t.false(builtFileContent2.includes(`"id1/test.js":`));
-
-
- // #4 build (with cache, no changes)
- // Run a self-contained build but with a different config which defines a custom preload.
- // The build should run and the output should still contain the expected self-contained bundle
- // (tasks "generateComponentPreload" and "generateLibraryPreload" should not get executed):
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-custom-preload-config.yaml"},
- config: {destPath, cleanDest: true, selfContained: "self-contained",
- dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "application.a": {}
- }
- }
- });
-
- // Check that output contains still the correct content:
- const builtFileContent3 = await fs.readFile(`${destPath}/resources/sap-ui-custom.js`, {encoding: "utf8"});
- t.false(builtFileContent3.includes(`"id1/test.js":`));
-
-
- // #5 build (with cache, no changes)
- // Run a self-contained build but without dependencies:
- // (everything should get skipped)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5.yaml"},
- config: {destPath, cleanDest: true, selfContained: "self-contained"},
- assertions: {
- projects: {}
- }
- });
-});
-
-test.serial("Build application.a (Custom bundling)", async (t) => {
- const fixtureTester = new FixtureTester(t, "application.a");
- const destPath = fixtureTester.destPath;
-
- // In this test, we're testing the behavior of a custom bundling configuration
- // which is defined in "ui5-custom-bundling.yaml".
- // This config generates a custom bundle in various modes.
- // The bundle includes resources by a filter ("Component.js" & "newFile.js") which are added at #3 and #4 build.
-
- // #1 build with custom bundle configuration (with empty cache)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-custom-bundling.yaml"},
- config: {destPath, cleanDest: false},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {}
- }
- }
- });
-
- // Verify that the custom bundle was created and contains the expected content:
- const customBundlePath = `${destPath}/resources/custom-bundle.js`;
- const customBundleContent = await fs.readFile(customBundlePath, {encoding: "utf8"});
- t.false(customBundleContent.includes("sap.ui.predefine("),
- "preload Mode: Custom bundle should not contain sap.ui.predefine() at this stage"
- );
- t.false(customBundleContent.includes("sap.ui.require.preload("),
- "preload Mode: Custom bundle should not contain sap.ui.require.preload() at this stage"
- );
- // Verify that source map was created:
- const sourceMapPath = `${destPath}/resources/custom-bundle.js.map`;
- const sourceMapContent = JSON.parse(await fs.readFile(sourceMapPath, {encoding: "utf8"}));
- t.true(sourceMapContent.sections.length === 0, "Source map file should not have content at this stage");
-
-
- // #2 build with custom bundle (with cache, no changes)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-custom-bundling.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}
- }
- });
-
-
- // Add a source file which is matched by the filter (UI5 Module):
- const newComponentFilepath = `${fixtureTester.fixturePath}/webapp/Component.js`;
- await fs.appendFile(newComponentFilepath,
- `sap.ui.define(["sap/ui/core/UIComponent", "sap/ui/core/ComponentSupport"], (UIComponent) => {
- "use strict";
- return UIComponent.extend("id1.Component", {
- metadata: {
- manifest: "json",
- interfaces: ["sap.ui.core.IAsyncContentCreation"],
- }
- });
-});`);
-
- // #3 build with custom bundle (with cache, with changes)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-custom-bundling.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "application.a": {
- skippedTasks: [
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "generateFlexChangesBundle",
- "replaceCopyright",
- "generateVersionInfo",
- ]
- }
- }
- }
- });
-
- // Verify that the updated custom bundle contains the change:
- // (the bundle should now contain sap.ui.predefine() due to the added UI5 module "Component.js")
- const customBundleContent2 = await fs.readFile(customBundlePath, {encoding: "utf8"});
- t.true(customBundleContent2.includes("sap.ui.predefine("),
- "preload Mode: Custom bundle should contain sap.ui.predefine() now"
- );
- t.false(customBundleContent2.includes("sap.ui.require.preload("),
- "preload Mode: Custom bundle should not contain sap.ui.require.preload() at this stage"
- );
- // Verify that source map was created and contains the change:
- const sourceMapContent2 = JSON.parse(await fs.readFile(sourceMapPath, {encoding: "utf8"}));
- t.true(sourceMapContent2.sections.length > 0, "Source map file should have content now");
-
-
- // Add another source file which is matched by the filter (non-UI5 module):
- const newTestFilepath = `${fixtureTester.fixturePath}/webapp/newFile.js`;
- await fs.appendFile(newTestFilepath, `console.log("another source file");`);
-
- // #4 build with custom bundle (with cache, with changes)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-custom-bundling.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "application.a": {
- skippedTasks: [
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "generateFlexChangesBundle",
- "replaceCopyright",
- "generateVersionInfo",
- ]
- }
- }
- }
- });
-
- // Verify that the custom bundle was created and contains the expected content:
- // (the bundle should now contain sap.ui.require.preload() due to the added non-UI5 module "newFile.js")
- const customBundleContent3 = await fs.readFile(customBundlePath, {encoding: "utf8"});
- t.true(customBundleContent3.includes("sap.ui.predefine("),
- "preload Mode: Custom bundle should contain sap.ui.predefine() still"
- );
- t.true(customBundleContent3.includes("sap.ui.require.preload("),
- "preload Mode: Custom bundle should contain sap.ui.require.preload() now"
- );
- // Verify that source map was created and contains the change:
- const sourceMapContent3 = JSON.parse(await fs.readFile(sourceMapPath, {encoding: "utf8"}));
- t.true(sourceMapContent3.sections.length > 0, "Source map file should have content still");
-
-
- // ----------------------------------------------------------------------------------
- // ---------------------------- Test other bundle modes: ----------------------------
- // ----------------------------------------------------------------------------------
- // Switch to "raw" mode:
- const ui5YamlContent = await fs.readFile(`${fixtureTester.fixturePath}/ui5-custom-bundling.yaml`);
- await fs.writeFile(`${fixtureTester.fixturePath}/ui5-custom-bundling.yaml`,
- ui5YamlContent.toString().replace(`- mode: preload`, `- mode: raw`));
-
- // #5 build with custom bundle configuration (with empty cache)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-custom-bundling.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "application.a": {}
- }
- }
- });
-
- // Verify that the custom bundle was created and contains the expected content:
- // (the bundle should contain sap.ui.define() now)
- const customBundleContent4 = await fs.readFile(customBundlePath, {encoding: "utf8"});
- t.false(customBundleContent4.includes("sap.ui.require.preload("),
- "raw Mode: Custom bundle should not contain sap.ui.require.preload() anymore"
- );
- t.false(customBundleContent4.includes("sap.ui.predefine("),
- "raw Mode: Custom bundle should not contain sap.ui.predefine() anymore"
- );
- t.true(customBundleContent4.includes("sap.ui.define("),
- "raw Mode: Custom bundle should contain sap.ui.define() now"
- );
- t.true(customBundleContent4.includes(`console.log("another source file");`));
- t.true(customBundleContent4.includes(`id1.Component`));
- // Verify that source map was created and contains the change:
- const sourceMapContent4 = JSON.parse(await fs.readFile(sourceMapPath, {encoding: "utf8"}));
- t.true(sourceMapContent4.sections.length > 0, "Source map file should have content still");
-
-
- // Switch to "require" mode:
- const ui5YamlContent2 = await fs.readFile(`${fixtureTester.fixturePath}/ui5-custom-bundling.yaml`);
- await fs.writeFile(`${fixtureTester.fixturePath}/ui5-custom-bundling.yaml`,
- ui5YamlContent2.toString().replace(`- mode: raw`, `- mode: require`));
-
- // #6 build with custom bundle configuration (with empty cache)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-custom-bundling.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "application.a": {}
- }
- }
- });
-
- // Verify that the custom bundle was created and contains the expected content:
- // (the bundle should contain sap.ui.require() now)
- const customBundleContent5 = await fs.readFile(customBundlePath, {encoding: "utf8"});
- t.false(customBundleContent5.includes("sap.ui.require.preload("),
- "require Mode: Custom bundle should not contain sap.ui.require.preload() anymore"
- );
- t.false(customBundleContent5.includes("sap.ui.predefine("),
- "require Mode: Custom bundle should not contain sap.ui.predefine() anymore"
- );
- t.false(customBundleContent5.includes("sap.ui.define("),
- "require Mode: Custom bundle should not contain sap.ui.define() anymore"
- );
- t.true(customBundleContent5.includes("sap.ui.require("),
- "require Mode: Custom bundle should contain sap.ui.require() now"
- );
- t.true(customBundleContent5.includes(`id1/newFile`));
- t.false(customBundleContent5.includes(`console.log("another source file");`));
- t.true(customBundleContent5.includes(`id1/Component`));
- // Verify that source map was created and contains the change:
- const sourceMapContent5 = JSON.parse(await fs.readFile(sourceMapPath, {encoding: "utf8"}));
- t.true(sourceMapContent5.sections.length > 0, "Source map file should have content still");
-
-
- // Switch to "bundleInfo" mode:
- const ui5YamlContent3 = await fs.readFile(`${fixtureTester.fixturePath}/ui5-custom-bundling.yaml`);
- await fs.writeFile(`${fixtureTester.fixturePath}/ui5-custom-bundling.yaml`,
- ui5YamlContent3.toString().replace(`- mode: require`, `- mode: bundleInfo`));
-
- // #7 build with custom bundle configuration (with empty cache)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-custom-bundling.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "application.a": {}
- }
- }
- });
-
- // Verify that the custom bundle was created and contains the expected content:
- // (the bundle should contain sap.ui.loader.config() now)
- const customBundleContent6 = await fs.readFile(customBundlePath, {encoding: "utf8"});
- t.false(customBundleContent6.includes("sap.ui.require.preload("),
- "bundleInfo Mode: Custom bundle should not contain sap.ui.require.preload() anymore"
- );
- t.false(customBundleContent6.includes("sap.ui.predefine("),
- "bundleInfo Mode: Custom bundle should not contain sap.ui.predefine() anymore"
- );
- t.false(customBundleContent6.includes("sap.ui.define("),
- "bundleInfo Mode: Custom bundle should not contain sap.ui.define() anymore"
- );
- t.false(customBundleContent6.includes("sap.ui.require("),
- "bundleInfo Mode: Custom bundle should not contain sap.ui.require() anymore"
- );
- t.true(customBundleContent6.includes("sap.ui.loader.config({bundlesUI5:{"),
- "bundleInfo Mode: Custom bundle should contain sap.ui.loader.config() now"
- );
- t.true(customBundleContent6.includes(`id1/newFile`));
- t.false(customBundleContent6.includes(`console.log("another source file");`));
- t.true(customBundleContent6.includes(`id1/Component`));
- // Verify that source map was created and contains the change:
- const sourceMapContent6 = JSON.parse(await fs.readFile(sourceMapPath, {encoding: "utf8"}));
- t.true(sourceMapContent6.sections.length > 0, "Source map file should have content still");
-});
-
-test.serial("Build library.d project multiple times", async (t) => {
- const fixtureTester = new FixtureTester(t, "library.d");
- const destPath = fixtureTester.destPath;
-
- // #1 build (with empty cache)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: false},
- assertions: {
- projects: {"library.d": {}}
- }
- });
-
-
- // #2 build (with cache, no changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}
- }
- });
-
-
- // Change a source file in library.d
- const changedFilePath = `${fixtureTester.fixturePath}/main/src/library/d/.library`;
- await fs.writeFile(
- changedFilePath,
- (await fs.readFile(changedFilePath, {encoding: "utf8"})).replace(
- `Library D`,
- `Library D (updated #1)`
- )
- );
-
- // #3 build (with cache, with changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {"library.d": {
- skippedTasks: [
- "buildThemes",
- "escapeNonAsciiCharacters",
- "minify",
- "replaceBuildtime",
- ]
- }}
- }
- });
-
- // Check whether the changes are in the destPath
- const builtFileContent = await fs.readFile(`${destPath}/resources/library/d/.library`, {encoding: "utf8"});
- t.true(
- builtFileContent.includes(`Library D (updated #1)`),
- "Build dest contains changed file content"
- );
-
- // Check whether the manifest.json was updated with the new documentation
- const manifestContent = await fs.readFile(`${destPath}/resources/library/d/manifest.json`, {encoding: "utf8"});
- t.true(
- manifestContent.includes(`"Library D (updated #1)"`),
- "Build dest contains updated description in manifest.json"
- );
-
-
- // #4 build (with cache, no changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}
- }
- });
-
-
- // Update copyright in ui5.yaml (should trigger a full rebuild of the project)
- const ui5YamlPath = `${fixtureTester.fixturePath}/ui5.yaml`;
- await fs.writeFile(
- ui5YamlPath,
- (await fs.readFile(ui5YamlPath, {encoding: "utf8"})).replace(
- "copyright: Some fancy copyright",
- "copyright: Some updated fancy copyright"
- )
- );
-
- await fs.writeFile(`${fixtureTester.fixturePath}/main/src/library/d/someNew.js`,
- `console.log("SOME NEW CONTENT");\n`
- );
-
- // #5 build (with cache, with changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {"library.d": {}}
- }
- });
-
- await fs.rm(`${fixtureTester.fixturePath}/main/src/library/d/someNew.js`);
-
- // #6 build (with cache, with changes - someNew.js removed)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {"library.d": {
- skippedTasks: [
- "buildThemes",
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "replaceBuildtime",
- ]
- }},
- }
- });
-
- // Re-add someNew.js (restores source state to match build #5)
- await fs.writeFile(`${fixtureTester.fixturePath}/main/src/library/d/someNew.js`,
- `console.log("SOME NEW CONTENT");\n`
- );
-
- // #7 build (with cache, with changes - someNew.js re-added)
- // Source state now matches build #5's cached result -> cache reused
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {},
- }
- });
-
- // Remove someNew.js again
- await fs.rm(`${fixtureTester.fixturePath}/main/src/library/d/someNew.js`);
-
- // #8 build (with cache, with changes - someNew.js removed again)
- // Source state matches build #6's cached result -> cache reused, everything skipped
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {},
- }
- });
-});
-
-test.serial("Build library.d (with various dependencies)", async (t) => {
- const fixtureTester = new FixtureTester(t, "library.d");
- const destPath = fixtureTester.destPath;
-
- // #1 build (with empty cache)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: false},
- assertions: {
- projects: {"library.d": {}}
- }
- });
-
-
- // #2 build (with cache, no changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}
- }
- });
-
-
- // Add a "library" dependency to library.d:
- await fixtureTester.addLibraryDependency(`${fixtureTester.fixturePath}/main/src/library/d`);
-
- // #3 build (no cache, with changes, with dependencies)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "library.z": {},
- "library.d": {
- skippedTasks: [
- "buildThemes",
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "replaceBuildtime",
- ]
- },
- }
- }
- });
-
-
- // Add a "themelib" dependency to library.d:
- await fixtureTester.addThemeLibraryDependency(`${fixtureTester.fixturePath}/main/src/library/d`);
-
- // #4 build (no cache, with changes, with dependencies)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "themelib.z": {},
- "library.d": {
- skippedTasks: [
- "buildThemes",
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "replaceBuildtime",
- ]
- }
- }
- }
- });
-});
-
-test.serial("Build library.d (Custom Library preload configuration)", async (t) => {
- const fixtureTester = new FixtureTester(t, "library.d");
- const destPath = fixtureTester.destPath;
-
- // In this test, we're testing the behavior of a custom preload configuration
- // which is defined in "ui5-custom-preload-config.yaml".
- // This custom preload configuration generates a library-preload.js similar to a default one.
- // However, it will omit a resource ("some.js") from the bundle.
-
- // ui5-custom-preload-config.yaml uses specVersion 5.0, which requires a manifest.json
- await fixtureTester._initialize();
- await fs.writeFile(`${fixtureTester.fixturePath}/main/src/library/d/manifest.json`,
- JSON.stringify({"sap.app": {"id": "library.d", "type": "library"}}, null, "\t"));
-
- // #1 build (no cache, no changes)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-custom-preload-config.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "library.d": {}
- }
- }
- });
-
- // Check that generated preload bundle doesn't contain the omitted file:
- t.false((await fs.readFile(`${destPath}/resources/library/d/library-preload.js`, {encoding: "utf8"}))
- .includes("library/d/some.js"));
-
-
- // #2 build (with cache, no changes)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-custom-preload-config.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}
- }
- });
-});
-
-test.serial("Build theme.library.e project multiple times", async (t) => {
- const fixtureTester = new FixtureTester(t, "theme.library.e");
- const destPath = fixtureTester.destPath;
-
- // #1 build (with empty cache)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: false},
- assertions: {
- projects: {"theme.library.e": {}}
- }
- });
-
-
- // #2 build (with cache, no changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}
- }
- });
-
-
- // Change a source file in theme.library.e
- const librarySourceFilePath =
- `${fixtureTester.fixturePath}/src/theme/library/e/themes/my_theme/library.source.less`;
- await fs.appendFile(librarySourceFilePath, `\n.someNewClass {\n\tcolor: red;\n}\n`);
-
- // #3 build (with cache, with changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {"theme.library.e": {}}
- }
- });
-
- // Check whether the changed file is in the destPath
- const builtFileContent = await fs.readFile(
- `${destPath}/resources/theme/library/e/themes/my_theme/library.source.less`, {encoding: "utf8"}
- );
- t.true(
- builtFileContent.includes(`.someNewClass`),
- "Build dest contains changed file content"
- );
-
- // Check whether the build output contains the new CSS rule
- const builtCssContent = await fs.readFile(
- `${destPath}/resources/theme/library/e/themes/my_theme/library.css`, {encoding: "utf8"}
- );
- t.true(
- builtCssContent.includes(`.someNewClass`),
- "Build dest contains new rule in library.css"
- );
-
-
- // Add a new less file and import it in library.source.less
- await fs.writeFile(`${fixtureTester.fixturePath}/src/theme/library/e/themes/my_theme/newImportFile.less`,
- `.someOtherNewClass {\n\tcolor: blue;\n}\n`
- );
- await fs.appendFile(librarySourceFilePath, `\n@import "newImportFile.less";\n`);
-
- // #4 build (with cache, with changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {"theme.library.e": {}},
- }
- });
-
- // Check whether the build output contains the import to the new file
- const builtCssContent2 = await fs.readFile(
- `${destPath}/resources/theme/library/e/themes/my_theme/library.css`, {encoding: "utf8"}
- );
- t.true(
- builtCssContent2.includes(`.someOtherNewClass`),
- "Build dest contains new rule in library.css"
- );
-
-
- // #5 build (with cache, no changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {},
- }
- });
-
-
- // Change content of new less file
- await fs.writeFile(`${fixtureTester.fixturePath}/src/theme/library/e/themes/my_theme/newImportFile.less`,
- `.someOtherNewClass {\n\tcolor: green;\n}\n`
- );
-
- // #6 build (with cache, with changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {"theme.library.e": {}},
- }
- });
-
- // Check whether the build output contains the changed content of the imported file
- const builtCssContent3 = await fs.readFile(
- `${destPath}/resources/theme/library/e/themes/my_theme/library.css`, {encoding: "utf8"}
- );
- t.true(
- builtCssContent3.includes(`.someOtherNewClass{color:green}`),
- "Build dest contains new rule in library.css"
- );
-
-
- // Delete import of library.source.less
- const librarySourceFileContent = (await fs.readFile(librarySourceFilePath)).toString();
- await fs.writeFile(librarySourceFilePath,
- librarySourceFileContent.replace(`\n@import "newImportFile.less";\n`, "")
- );
-
- // Change content of new less file again
- await fs.writeFile(`${fixtureTester.fixturePath}/src/theme/library/e/themes/my_theme/newImportFile.less`,
- `.someOtherNewClass {\n\tcolor: yellow;\n}\n`
- );
-
- // #7 build (with cache, with changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {"theme.library.e": {
- skippedTasks: ["buildThemes"]
- }},
- }
- });
-
- // Check if library.css does NOT contain the imported rule anymore
- t.false(
- (await fs.readFile(
- `${destPath}/resources/theme/library/e/themes/my_theme/library.css`, {encoding: "utf8"}
- )).includes(`.someOtherNewClass`),
- "Build dest should NOT contain the rule in library.css anymore"
- );
-
-
- // Delete the imported less file
- await fs.rm(`${fixtureTester.fixturePath}/src/theme/library/e/themes/my_theme/newImportFile.less`);
-
- // #8 build (with cache, with changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}, // -> everything should be skipped
- }
- });
-});
-
-test.serial("Build theme.library.e (with various dependencies)", async (t) => {
- const fixtureTester = new FixtureTester(t, "theme.library.e");
- const destPath = fixtureTester.destPath;
-
- // #1 build (with empty cache)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: false, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {"theme.library.e": {}}
- }
- });
-
-
- // #2 build (with cache, no changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {}
- }
- });
-
-
- // Add a "library" dependency to theme.library.e:
- await fixtureTester.addLibraryDependency(`${fixtureTester.fixturePath}/src/theme/library/e/themes/my_theme`);
-
- // #3 build (no cache, with changes, with dependencies)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "library.z": {},
- "theme.library.e": {
- skippedTasks: [
- "buildThemes",
- "replaceCopyright",
- "replaceVersion",
- ]
- },
- }
- }
- });
-});
-
-test.serial("Build component.a project multiple times", async (t) => {
- const fixtureTester = new FixtureTester(t, "component.a");
- const destPath = fixtureTester.destPath;
-
- // #1 build (no cache, no changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "component.a": {}
- }
- }
- });
-
-
- // #2 build (with cache, no changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}
- }
- });
-
-
- // Change a source file in component.a
- const changedFilePath = `${fixtureTester.fixturePath}/src/test.js`;
- await fs.appendFile(changedFilePath, `\ntest("line added");\n`);
-
- // #3 build (with cache, with changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "component.a": {
- skippedTasks: [
- "escapeNonAsciiCharacters",
- // Note: replaceCopyright is skipped because no copyright is configured in the project
- "replaceCopyright",
- "enhanceManifest",
- "generateFlexChangesBundle",
- ]
- }
- }
- }
- });
-
- // Check whether the changed file is in the destPath
- const builtFileContent = await fs.readFile(`${destPath}/resources/id1/test.js`, {encoding: "utf8"});
- t.true(builtFileContent.includes(`test("line added");`), "Build dest contains changed file content");
-
-
- // #4 build (with cache, no changes, with dependencies)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- }
- }
- });
-
-
- // #5 build (with cache, no changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}
- }
- });
-
-
- // #6 build (with cache, no changes, with dependencies)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {}
- }
- });
-
-
- // Add a new file to component.a
- await fs.writeFile(`${fixtureTester.fixturePath}/src/someNew.js`,
- `console.log("SOME NEW CONTENT");\n`
- );
-
- // #7 build (with cache, with changes - someNew.js added)
- // Tasks that don't depend on someNew.js can reuse their caches from build #3.
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {"component.a": {
- skippedTasks: [
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "generateFlexChangesBundle",
- "replaceCopyright",
- ]
- }}
- }
- });
-
- await fs.rm(`${fixtureTester.fixturePath}/src/someNew.js`);
-
- // #8 build (with cache, with changes - someNew.js removed)
- // Source state matches build #6's cached result -> cache reused, everything skipped
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {},
- }
- });
-});
-
-test.serial("Build component.a (with various dependencies)", async (t) => {
- const fixtureTester = new FixtureTester(t, "component.a");
- const destPath = fixtureTester.destPath;
-
- // #1 build (with empty cache)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: false, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "component.a": {}
- }
- }
- });
-
-
- // #2 build (with cache, no changes, with dependencies)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {}
- }
- });
-
-
- // Add a "component" dependency to component.a:
- await fixtureTester.addComponentDependency(`${fixtureTester.fixturePath}/src`);
-
- // #3 build (no cache, with changes, with dependencies)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "component.z": {},
- "component.a": {
- skippedTasks: [
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "generateFlexChangesBundle",
- "replaceCopyright",
- ]
- }
- }
- }
- });
-
-
- // Add a "library" dependency to component.a:
- await fixtureTester.addLibraryDependency(`${fixtureTester.fixturePath}/src`);
-
- // #4 build (no cache, with changes, with dependencies)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "library.z": {},
- "component.a": {
- skippedTasks: [
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "generateFlexChangesBundle",
- "replaceCopyright",
- ]
- }
- }
- }
- });
-
-
- // Add a "themelib" dependency to component.a:
- await fixtureTester.addThemeLibraryDependency(`${fixtureTester.fixturePath}/src`);
-
- // #5 build (no cache, with changes, with dependencies)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "themelib.z": {},
- "component.a": {
- skippedTasks: [
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "generateFlexChangesBundle",
- "replaceCopyright",
- ]
- }
- }
- }
- });
-
-
- // Add a "module" dependency to component.a:
- await fixtureTester.addModuleDependency(`${fixtureTester.fixturePath}/src`);
-
- // #6 build (no cache, with changes, with dependencies)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "module.z": {},
- "component.a": {
- skippedTasks: [
- "enhanceManifest",
- "escapeNonAsciiCharacters",
- "generateFlexChangesBundle",
- "replaceCopyright",
- ]
- }
- }
- }
- });
-});
-
-test.serial("Build component.a (Custom Component preload configuration)", async (t) => {
- const fixtureTester = new FixtureTester(t, "component.a");
- const destPath = fixtureTester.destPath;
-
- // In this test, we're testing the behavior of a custom preload configuration
- // which is defined in "ui5-custom-preload-config.yaml".
- // This custom preload configuration generates a Component-preload.js similar to a default one.
- // However, it will omit a resource ("test.js") from the bundle.
-
- // #1 build (no cache, no changes)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-custom-preload-config.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "component.a": {}
- }
- }
- });
-
- // Check that generated preload bundle doesn't contain the omitted file:
- t.false((await fs.readFile(`${destPath}/resources/id1/Component-preload.js`, {encoding: "utf8"}))
- .includes("id1/test.js"));
-
-
- // #2 build (with cache, no changes)
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-custom-preload-config.yaml"},
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}
- }
- });
-});
-
-test.serial("Build module.b project multiple times", async (t) => {
- const fixtureTester = new FixtureTester(t, "module.b");
- const destPath = fixtureTester.destPath;
-
- // #1 build (no cache, no changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {"module.b": {}}
- },
- });
-
-
- // #2 build (with cache, no changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}
- }
- });
-
-
- // Change a source file in module.b
- const changedFilePath = `${fixtureTester.fixturePath}/dev/devTools.js`;
- await fs.appendFile(changedFilePath, `\ntest("line added");\n`);
-
- // #3 build (no cache, with changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {"module.b": {}}
- }
- });
-
- // Check whether the changed file is in the destPath
- const builtFileContent = await fs.readFile(`${destPath}/resources/b/module/dev/devTools.js`, {encoding: "utf8"});
- t.true(builtFileContent.includes(`test("line added");`), "Build dest contains changed file content");
-
-
- // Remove a source file in module.b
- await fs.rm(`${fixtureTester.fixturePath}/dev/devTools.js`);
-
- // #4 build (no cache, with changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {"module.b": {}}
- }
- });
-
- // Check that the removed file is NOT in the destPath anymore
- // (dist output should be totally empty: no source files -> no build result)
- await t.throwsAsync(fs.readFile(`${destPath}/resources/b/module/dev/devTools.js`, {encoding: "utf8"}));
-
-
- // #5 build (with cache, no changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}
- }
- });
-
-
- // Add a new file in module.b
- await fs.mkdir(`${fixtureTester.fixturePath}/dev/newFolder`, {recursive: true});
- await fs.writeFile(`${fixtureTester.fixturePath}/dev/newFolder/newFile.js`,
- `console.log("this is a new file which should be included in the build result")`);
-
- // #6 build (no cache, with changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {"module.b": {}}
- },
- });
-
- // Check whether the added file is in the destPath
- const newFile = await fs.readFile(`${destPath}/resources/b/module/dev/newFolder/newFile.js`,
- {encoding: "utf8"});
- t.true(newFile.includes(`this is a new file which should be included in the build result`),
- "Build dest contains correct file content");
-
-
- // Add a new path mapping:
- const originalUi5Yaml = await fs.readFile(`${fixtureTester.fixturePath}/ui5.yaml`, {encoding: "utf8"}); // for later
- const newFileName = "someOtherNewFile.js";
- const newFolderName = "newPathmapping";
- const virtualPath = `/resources/b/module/${newFolderName}/`;
- await fs.writeFile(`${fixtureTester.fixturePath}/ui5.yaml`,
- `---
-specVersion: "5.0"
-type: module
-metadata:
- name: module.b
-resources:
- configuration:
- paths:
- /resources/b/module/dev/: dev
- ${virtualPath}: ${newFolderName}`
- );
-
- // Create a resource for this new path mapping:
- await fs.mkdir(`${fixtureTester.fixturePath}/${newFolderName}`, {recursive: true});
- await fs.writeFile(`${fixtureTester.fixturePath}/${newFolderName}/${newFileName}`,
- `console.log("this should be included in the build result if the path mapping has been set")`);
-
- // #7 build (no cache, with changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {"module.b": {}}
- },
- });
-
- // Check whether the added file is in the destPath
- const someOtherNewFile = await fs.readFile(`${destPath}${virtualPath}${newFileName}`,
- {encoding: "utf8"});
- t.true(someOtherNewFile.includes(`path mapping has been set`), "Build dest contains correct file content");
-
-
- // Remove the path mapping again (revert original ui5.yaml):
- await fs.writeFile(`${fixtureTester.fixturePath}/ui5.yaml`, originalUi5Yaml);
-
- // #8 build (with cache, with changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {} // -> cache can be reused
- },
- });
-
- // Check that the added resource of the path mapping is NOT in the destPath anymore:
- await t.throwsAsync(fs.readFile(`${destPath}${virtualPath}${newFileName}`,
- {encoding: "utf8"}));
-
-
- // #9 build (with cache, no changes, with dependencies)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- }
- },
- });
-});
-
-test.serial("Build module.b (with various dependencies)", async (t) => {
- const fixtureTester = new FixtureTester(t, "module.b");
- const destPath = fixtureTester.destPath;
-
- // #1 build (no cache, no changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "module.b": {}
- }
- },
- });
-
-
- // #2 build (with cache, no changes)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {}
- }
- });
-
-
- // Add a "library" dependency to module.b:
- await fixtureTester.addLibraryDependency(`${fixtureTester.fixturePath}/dev`);
-
- // #3 build (no cache, with changes, with dependencies)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "library.z": {},
- "module.b": {}
- }
- },
- });
-
-
- // Add a "themelib" dependency to module.b:
- await fixtureTester.addThemeLibraryDependency(`${fixtureTester.fixturePath}/dev`);
-
- // #4 build (no cache, with changes, with dependencies)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "themelib.z": {},
- "module.b": {}
- }
- }
- });
-});
-
-test.serial("Build race condition: file modified during active build", async (t) => {
- const fixtureTester = new FixtureTester(t, "application.a");
- const destPath = fixtureTester.destPath;
- await fixtureTester._initialize();
- const testFilePath = `${fixtureTester.fixturePath}/webapp/test.js`;
- const originalContent = await fs.readFile(testFilePath, {encoding: "utf8"});
- const addedFileName = "added-during-build.js";
- const addedFilePath = `${fixtureTester.fixturePath}/webapp/${addedFileName}`;
-
- // #1 Build with race condition triggered by custom task that modifies test.js during the build.
- // The build should detect the source change and throw.
- const error1 = await t.throwsAsync(fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-race-condition.yaml"},
- config: {destPath, cleanDest: true},
- }));
- t.true(error1.message.includes("Detected changes to source files of project application.a during the build"),
- "Error message indicates source change detected");
-
- // #2 Revert the source file to original content
- await fs.writeFile(testFilePath, originalContent);
-
- // #3 Build again with normal config after reverting the source.
- // Since the race condition build threw, no corrupted cache was written.
- // This build should succeed and produce clean output.
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {
- "application.a": {}
- }
- }
- });
-
- // Verify the output does NOT contain the race condition modification
- const finalBuiltContent = await fs.readFile(`${destPath}/test.js`, {encoding: "utf8"});
- t.false(
- finalBuiltContent.includes(`RACE CONDITION MODIFICATION`),
- "Build output does not contain race condition modification after clean rebuild"
- );
-
- // #4 Build with race condition triggered by add-file custom task
- await fs.rm(addedFilePath, {force: true});
- const error2 = await t.throwsAsync(fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-race-condition-add-file.yaml"},
- config: {destPath, cleanDest: true},
- }));
- t.true(error2.message.includes("Detected changes to source files of project application.a during the build"),
- "Error message indicates source change detected (add file)");
-
- // #5 Revert source state by removing the file that was added during build
- await fs.rm(addedFilePath, {force: true});
-
- // #6 Build again with normal config after reverting.
- // Cache from build #3 is still valid (same source state), so everything should be skipped.
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}
- }
- });
-
- // #7 Build with race condition triggered by delete-file custom task
- const error3 = await t.throwsAsync(fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-race-condition-delete-file.yaml"},
- config: {destPath, cleanDest: true},
- }));
- t.true(error3.message.includes("Detected changes to source files of project application.a during the build"),
- "Error message indicates source change detected (delete file)");
-
- // #8 Revert source state by restoring the deleted file
- await fs.writeFile(testFilePath, originalContent);
-
- // #9 Build again with normal config after restoring.
- // Cache from build #3 is still valid (same source state), so everything should be skipped.
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true},
- assertions: {
- projects: {}
- }
- });
-
- // Verify test.js is present in output
- const restoredBuiltFileContent = await fs.readFile(`${destPath}/test.js`, {encoding: "utf8"});
- t.true(
- restoredBuiltFileContent.includes(`console.log`),
- "Build output contains restored file after source recovery"
- );
-});
-
-test.serial("Build dependency race condition: frozen source reader protects against filesystem changes",
- async (t) => {
- const fixtureTester = new FixtureTester(t, "application.a");
- const destPath = fixtureTester.destPath;
-
- // Build with dependency-race-condition custom task and all dependencies included.
- // library.d is built first → its sources are frozen in CAS.
- // Then application.a builds, running the custom task that:
- // 1. Reads library.d's some.js via the dependency reader (CAS-backed)
- // 2. Modifies some.js on disk
- // 3. Re-reads via the dependency reader
- // 4. Asserts the content is still the original CAS-frozen content (not modified disk)
- // 5. Restores the file on disk
- // If the frozen reader is not working, the custom task throws and the build fails.
- await fixtureTester.buildProject({
- graphConfig: {rootConfigPath: "ui5-dependency-race-condition.yaml"},
- config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {}
- }
- }
- });
-
- // Sanity check: verify library.d's some.js exists in build output
- const builtContent = await fs.readFile(`${destPath}/resources/library/d/some.js`, {encoding: "utf8"});
- t.truthy(builtContent, "library.d some.js exists in build output");
- });
-
-test.serial("Build with dependencies: Verify sap-ui-version.json generation and regeneration", async (t) => {
- const fixtureTester = new FixtureTester(t, "application.a");
- const destPath = fixtureTester.destPath;
- const versionInfoPath = `${destPath}/resources/sap-ui-version.json`;
-
- // Build #1: Full build with all dependencies in JSDoc mode
- // JSDoc mode enables generateVersionInfo task which creates sap-ui-version.json
- await fixtureTester.buildProject({
- config: {
- destPath,
- cleanDest: true,
- jsdoc: "jsdoc",
- dependencyIncludes: {includeAllDependencies: true}
- },
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {}
- }
- }
- });
-
- const versionInfo1Content = await fs.readFile(versionInfoPath, {encoding: "utf8"});
- t.truthy(versionInfo1Content, "sap-ui-version.json should exist");
- const versionInfo1 = JSON.parse(versionInfo1Content);
-
- // Root project metadata
- t.is(versionInfo1.name, "application.a", "Root project name");
- t.is(versionInfo1.version, "1.0.0", "Root project version");
- t.is(typeof versionInfo1.buildTimestamp, "string", "buildTimestamp is string");
-
- // Libraries array
- t.true(Array.isArray(versionInfo1.libraries), "libraries is array");
- const libraryNames = versionInfo1.libraries.map((lib) => lib.name).sort();
- t.deepEqual(libraryNames, ["library.a", "library.b", "library.c", "library.d"],
- "Contains all dependency libraries");
-
- // Each library has required fields
- versionInfo1.libraries.forEach((lib) => {
- t.is(typeof lib.name, "string", `Library ${lib.name} has name`);
- t.is(typeof lib.version, "string", `Library ${lib.name} has version`);
- t.is(typeof lib.buildTimestamp, "string", `Library ${lib.name} has buildTimestamp`);
- });
-
- const firstBuildTimestamp = versionInfo1.buildTimestamp;
-
- // Build #2: No changes, expect full cache hit
- await fixtureTester.buildProject({
- config: {
- destPath,
- cleanDest: true,
- jsdoc: "jsdoc",
- dependencyIncludes: {includeAllDependencies: true}
- },
- assertions: {
- projects: {} // All projects cached
- }
- });
-
- // Verify sap-ui-version.json was reused from cache (timestamp unchanged)
- const versionInfo2Content = await fs.readFile(versionInfoPath, {encoding: "utf8"});
- const versionInfo2 = JSON.parse(versionInfo2Content);
- t.is(versionInfo2.buildTimestamp, firstBuildTimestamp,
- "buildTimestamp unchanged when cached (no source changes)");
-});
-
-test.serial("Build application.a with --cache=Default", async (t) => {
- const fixtureTester = new FixtureTester(t, "application.a");
- const destPath = fixtureTester.destPath;
-
- // #1 Build with empty cache --> all tasks execute
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: false, cache: Cache.Default},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {}
- }
- }
- });
-
- // #2 Build with valid cache, no changes --> nothing rebuilds (all cached)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, cache: Cache.Default},
- assertions: {
- projects: {}
- }
- });
-
- // Change a source file in application.a
- const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`;
- await fs.appendFile(changedFilePath, `\ntest("line added for cache test");\n`);
-
- // #3 Build with valid cache, source changes --> only affected tasks rebuild
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, cache: Cache.Default},
- assertions: {
- projects: {
- "application.a": {
- skippedTasks: [
- "escapeNonAsciiCharacters",
- "replaceCopyright",
- "enhanceManifest",
- "generateFlexChangesBundle",
- "generateVersionInfo",
- ]
- }
- }
- }
- });
-
- // Verify the changed file is in the destPath
- const builtFileContent = await fs.readFile(`${destPath}/test.js`, {encoding: "utf8"});
- t.true(builtFileContent.includes(`test("line added for cache test");`),
- "Build dest contains changed file content");
-});
-
-test.serial("Build application.a with --cache=Off", async (t) => {
- const fixtureTester = new FixtureTester(t, "application.a");
- const destPath = fixtureTester.destPath;
-
- // #1 Build with cache=Off --> all tasks execute, cache not written
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: false, cache: Cache.Off},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {},
- }
- }
- });
-
- // #2 Build with cache=Off (again) --> all tasks execute again (no cache reuse)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, cache: Cache.Off},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {},
- }
- }
- });
-
- // #3 Build with cache=Default --> all tasks execute (no cache from previous builds)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, cache: Cache.Default},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {},
- }
- }
- });
-
- // #4 Build with cache=Default (again) --> nothing rebuilds (cache now exists)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, cache: Cache.Default},
- assertions: {
- projects: {}
- }
- });
-
- // #5 Build with cache=Off --> all tasks execute (ignores existing cache)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, cache: Cache.Off},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {},
- }
- }
- });
-});
-
-test.serial("Build application.a with --cache=ReadOnly", async (t) => {
- const fixtureTester = new FixtureTester(t, "application.a");
- const destPath = fixtureTester.destPath;
-
- // #1 Build with cache=Default --> all tasks execute, cache written
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: false, cache: Cache.Default},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {}
- }
- }
- });
-
- // #2 Build with cache=ReadOnly, no changes --> nothing rebuilds (cache used)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, cache: Cache.ReadOnly},
- assertions: {
- projects: {}
- }
- });
-
- // Change a source file in application.a
- const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`;
- await fs.appendFile(changedFilePath, `\ntest("line added for ReadOnly test");\n`);
-
- // #3 Build with cache=ReadOnly --> affected tasks rebuild, BUT cache not updated
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, cache: Cache.ReadOnly},
- assertions: {
- projects: {
- "application.a": {
- skippedTasks: [
- "escapeNonAsciiCharacters",
- "replaceCopyright",
- "enhanceManifest",
- "generateFlexChangesBundle",
- "generateVersionInfo",
- ]
- }
- }
- }
- });
-
- // Verify the changed file is in the destPath
- const builtFileContent = await fs.readFile(`${destPath}/test.js`, {encoding: "utf8"});
- t.true(builtFileContent.includes(`test("line added for ReadOnly test");`),
- "Build dest contains changed file content");
-
- // #4 Build with cache=Default, no new changes --> rebuilds again (cache from #3 missing)
- // This validates that ReadOnly didn't write the cache in step #3
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, cache: Cache.Default},
- assertions: {
- projects: {
- "application.a": {
- skippedTasks: [
- "escapeNonAsciiCharacters",
- "replaceCopyright",
- "enhanceManifest",
- "generateFlexChangesBundle",
- "generateVersionInfo",
- ]
- }
- }
- }
- });
-});
-
-test.serial("Build application.a with --cache=Force (1)", async (t) => {
- const fixtureTester = new FixtureTester(t, "application.a");
- const destPath = fixtureTester.destPath;
-
- // #1: Build with cache=Default --> all tasks execute, cache written
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: false, cache: Cache.Default},
- assertions: {
- projects: {
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {}
- }
- }
- });
-
- // #2: Build with cache=Force, no changes --> nothing rebuilds (cache used)
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, cache: Cache.Force},
- assertions: {
- projects: {}
- }
- });
-
- // Change a source file in application.a
- const changedFilePath = `${fixtureTester.fixturePath}/webapp/test.js`;
- await fs.appendFile(changedFilePath, `\ntest("line added for Force test");\n`);
-
- // #3: Build with cache=Force --> ERROR (cache invalid due to source changes)
- const error = await t.throwsAsync(async () => {
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: true, cache: Cache.Force},
- });
- });
-
- t.truthy(error, "Build with Force mode should throw error when cache is stale");
- t.true(error.message.includes(`Cache is in "Force" mode but cache is stale for project application.a ` +
- `due to 1 changed source file(s). ` +
- `Use "Default", "ReadOnly" or "Off" to rebuild.`));
-});
-
-test.serial("Build application.a with --cache=Force (2)", async (t) => {
- const fixtureTester = new FixtureTester(t, "application.a");
- const destPath = fixtureTester.destPath;
-
- // #1: Build with cache=Force on empty cache --> ERROR with clear message
- const error = await t.throwsAsync(async () => {
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: false, cache: Cache.Force},
- });
- });
-
- t.truthy(error, "Build with Force mode should throw error when cache is empty");
- t.true(error.message.includes(`Cache is in "Force" mode but no cache found for project application.a. ` +
- `Use "Default", "ReadOnly" or "Off" to rebuild.`));
-});
-
-test.serial("Build application.a with --exclude-task=generateVersionInfo", async (t) => {
- // This test verifies that when task generateVersionInfo is excluded,
- // the sap-ui-version.json file is NOT generated in the output
- // AND the dependencies are not automatically built.
-
- const fixtureTester = new FixtureTester(t, "application.a");
- const destPath = fixtureTester.destPath;
-
- // #1 Build with empty cache and exclude generateVersionInfo task
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: false, excludedTasks: ["generateVersionInfo"]},
- assertions: {
- projects: {
- // Only application.a is built, dependencies are skipped
- "application.a": {}
- }
- }
- });
-
- // Verify that sap-ui-version.json does NOT exist in the output
- const versionInfoPath = `${destPath}/resources/sap-ui-version.json`;
- await t.throwsAsync(fs.readFile(versionInfoPath, {encoding: "utf8"}), undefined,
- "sap-ui-version.json should NOT exist when generateVersionInfo task is excluded");
-
-
- // #2 Build with empty cache and include all default tasks now
- await fixtureTester.buildProject({
- config: {destPath, cleanDest: false},
- assertions: {
- projects: {
- // All dependencies are built now
- "library.d": {},
- "library.a": {},
- "library.b": {},
- "library.c": {},
- "application.a": {}
- }
- }
- });
-
- // Verify that sap-ui-version.json DOES exist in the output now
- await t.notThrowsAsync(fs.readFile(versionInfoPath, {encoding: "utf8"}), undefined,
- "sap-ui-version.json should exist when generateVersionInfo task is included");
-});
-
-function getFixturePath(fixtureName) {
- return fileURLToPath(new URL(`../../fixtures/${fixtureName}`, import.meta.url));
-}
-
-function getTmpPath(folderName) {
- return fileURLToPath(new URL(`../../tmp/ProjectBuilder/${folderName}`, import.meta.url));
-}
-
-async function rmrf(dirPath) {
- return fs.rm(dirPath, {recursive: true, force: true, maxRetries: 3, retryDelay: 200});
-}
-
-class FixtureTester {
- constructor(t, fixtureName) {
- this._t = t;
- this._sinon = t.context.sinon;
- this._fixtureName = fixtureName;
- this._initialized = false;
-
- // Public
- this.fixturePath = getTmpPath(fixtureName);
- this.destPath = getTmpPath(`${fixtureName}/dist`);
- this.ui5DataDir = getTmpPath(`${fixtureName}/.ui5`);
- }
-
- async _initialize() {
- if (this._initialized) {
- return;
- }
- await rmrf(this.fixturePath); // Clean up any previous test runs
- await fs.cp(getFixturePath(this._fixtureName), this.fixturePath, {recursive: true});
- this._initialized = true;
- }
-
- async buildProject({graphConfig = {}, config = {}, assertions} = {}) {
- await this._initialize();
- this._sinon.resetHistory();
-
- const graph = await graphFromPackageDependencies({
- ...graphConfig,
- cwd: this.fixturePath,
- });
-
- // Execute the build
- await graph.build({...config, ui5DataDir: this.ui5DataDir});
-
- // Apply assertions if provided
- if (assertions) {
- this._assertBuild(assertions);
- }
- }
-
- _assertBuild(assertions) {
- /**
- * assertions object structure:
- * {
- * projects: {
- * "projectName": {
- * skippedTasks: ["task1", "task2"],
- * },
- * // ...
- * },
- * allProjects: ["projectName1", "projectName2"]
- * }
- *
- * projects - for asserting all projects which are expected to be built
- * allProjects - optional, for asserting all seen projects nonetheless if built or not
- */
- const {projects = {}, allProjects = []} = assertions;
-
- const projectsInOrder = [];
- const seenProjects = new Set();
- const tasksByProject = {};
-
- // Extract build status to identify built projects and their order
- const buildStatusEvents = this._t.context.buildStatusEventStub.args.map((args) => args[0]);
- for (const event of buildStatusEvents) {
- if (!seenProjects.has(event.projectName)) {
- seenProjects.add(event.projectName);
- if (event.status === "project-build-start") {
- projectsInOrder.push(event.projectName);
- }
- }
- }
-
- // Extract task status to identify skipped & executed tasks per project
- const projectBuildStatusEvents = this._t.context.projectBuildStatusEventStub.args.map((args) => args[0]);
- for (const event of projectBuildStatusEvents) {
- if (!tasksByProject[event.projectName]) {
- tasksByProject[event.projectName] = {executed: [], skipped: []};
- }
- if (event.status === "task-skip") {
- tasksByProject[event.projectName].skipped.push(event.taskName);
- } else if (event.status === "task-start") {
- tasksByProject[event.projectName].executed.push(event.taskName);
- }
- }
-
- // Assert built projects in order
- const expectedProjects = Object.keys(projects);
- this._t.deepEqual(projectsInOrder, expectedProjects);
-
- // Optional check: Assert seen projects
- if (allProjects.length > 0) {
- const expectedAllProjects = allProjects.sort();
- const actualAllProjects = Array.from(seenProjects).sort();
- this._t.deepEqual(actualAllProjects, expectedAllProjects,
- "All seen projects (built or not) should match expected");
- }
-
- // Assert skipped tasks per project
- for (const [projectName, expectedSkipped] of Object.entries(projects)) {
- const skippedTasks = expectedSkipped.skippedTasks || [];
- const actualSkipped = (tasksByProject[projectName]?.skipped || []).sort();
- const expectedArray = skippedTasks.sort();
- this._t.deepEqual(actualSkipped, expectedArray);
- }
- }
-
- /**
- * Helper function to add a new module dependency ("module.z") to an arbitrary root project.
- *
- * @param {string} sourceDir - source path of the root project (e.g. `${this.fixturePath}/webapp` for applications)
- */
- async addModuleDependency(sourceDir) {
- await fs.mkdir(`${this.fixturePath}/node_modules/module.z/dev`, {recursive: true});
- await fs.writeFile(`${this.fixturePath}/node_modules/module.z/dev/devTools.js`,
- `console.log("module.z devTools");`);
- await fs.writeFile(`${this.fixturePath}/node_modules/module.z/package.json`,
- `{
- "name": "module.z",
- "version": "1.0.0"
-}`
- );
- await fs.writeFile(`${this.fixturePath}/node_modules/module.z/ui5.yaml`,
- `---
-specVersion: "5.0"
-type: module
-metadata:
- name: module.z
-resources:
- configuration:
- paths:
- /resources/z/module/dev/: dev`);
-
- await fs.writeFile(`${sourceDir}/moduleConsumer.js`,
- `sap.ui.define(["z/module/dev/devTools"], () => {});`);
- const packageJsonContent = JSON.parse(
- await fs.readFile(`${this.fixturePath}/package.json`, {encoding: "utf8"}));
- if (!packageJsonContent.dependencies) {
- packageJsonContent.dependencies = {};
- }
- packageJsonContent.dependencies["module.z"] = "file:../module.z";
- await fs.writeFile(`${this.fixturePath}/package.json`,
- JSON.stringify(packageJsonContent)
- );
- }
-
- /**
- * Helper function to add a new component dependency ("component.z") to an arbitrary root project.
- *
- * @param {string} sourceDir - source path of the root project (e.g. `${this.fixturePath}/webapp` for applications)
- */
- async addComponentDependency(sourceDir) {
- await fs.mkdir(`${this.fixturePath}/node_modules/component.z/src`, {recursive: true});
- await fs.writeFile(`${this.fixturePath}/node_modules/component.z/src/Component.js`,
- `sap.ui.define(["sap/ui/core/UIComponent"], function(UIComponent){
- "use strict";
- return UIComponent.extend('component.z.Component', {
- createContent: function () {
- return new Label({ text: "Hello!" });
- }
- });
-});
-`);
- await fs.writeFile(`${this.fixturePath}/node_modules/component.z/src/manifest.json`,
- `{
- "_version": "1.1.0",
- "sap.app": {
- "_version": "1.1.0",
- "id": "component.z",
- "type": "component",
- "applicationVersion": {
- "version": "1.2.2"
- },
- "embeds": ["embedded"],
- "title": "{{title}}"
- }
-}`);
- await fs.writeFile(`${this.fixturePath}/node_modules/component.z/ui5.yaml`,
- `---
-specVersion: "5.0"
-type: component
-metadata:
- name: component.z`);
- await fs.writeFile(`${this.fixturePath}/node_modules/component.z/package.json`,
- `{
- "name": "component.z",
- "version": "1.0.0"
-}`
- );
-
- await fs.writeFile(`${sourceDir}/componentConsumer.js`,
- `sap.ui.define(["component/z"], () => {});`);
- const packageJsonContent = JSON.parse(
- await fs.readFile(`${this.fixturePath}/package.json`, {encoding: "utf8"}));
- if (!packageJsonContent.dependencies) {
- packageJsonContent.dependencies = {};
- }
- packageJsonContent.dependencies["component.z"] = "file:../component.z";
- await fs.writeFile(`${this.fixturePath}/package.json`,
- JSON.stringify(packageJsonContent)
- );
- }
-
- /**
- * Helper function to add a new library dependency ("library.z") to an arbitrary root project.
- *
- * @param {string} sourceDir - source path of the root project (e.g. `${this.fixturePath}/webapp` for applications)
- */
- async addLibraryDependency(sourceDir) {
- await fs.mkdir(`${this.fixturePath}/node_modules/library.z/src/library/z`, {recursive: true});
- await fs.writeFile(`${this.fixturePath}/node_modules/library.z/src/library/z/library.js`,
- `
-sap.ui.define([
- "sap/base/util/ObjectPath",
- "sap/ui/core/Core",
- "sap/ui/core/library"
-], function (ObjectPath, Core) {
- "use strict";
-
- Core.initLibrary({
- name: "library.z",
- version: ` + "\"${version}\"" + `,
- dependencies: [
- "sap.ui.core"
- ],
- types: [
- "library.z.ExampleColor"
- ],
- interfaces: [],
- elements: [],
- noLibraryCSS: false
- });
- const thisLib = ObjectPath.get("library.z");
-
- thisLib.ExampleColor = {
- Default : "Default",
- Highlight : "Highlight"
- };
- return thisLib;
-});`);
- await fs.writeFile(`${this.fixturePath}/node_modules/library.z/src/library/z/manifest.json`,
- JSON.stringify({"sap.app": {"id": "library.z", "type": "library"}}, null, "\t"));
- await fs.writeFile(`${this.fixturePath}/node_modules/library.z/src/library/z/.library`,
- `
-
-
- library.z
- SAP SE
- Some fancy copyright
- `+"${version}"+`
-
- Library Z
-
-`);
- await fs.writeFile(`${this.fixturePath}/node_modules/library.z/ui5.yaml`,
- `---
-specVersion: "5.0"
-type: library
-metadata:
- name: library.z
-`);
- await fs.writeFile(`${this.fixturePath}/node_modules/library.z/package.json`,
- `{
- "name": "library.z",
- "version": "1.0.0"
-}`
- );
-
- await fs.writeFile(`${sourceDir}/libraryConsumer.js`,
- `sap.ui.define(["library/z/library"],
- (LibraryZ) => {
- console.log(LibraryZ.ExampleColor.Default);
-});`);
- const packageJsonContent = JSON.parse(
- await fs.readFile(`${this.fixturePath}/package.json`, {encoding: "utf8"}));
- if (!packageJsonContent.dependencies) {
- packageJsonContent.dependencies = {};
- }
- packageJsonContent.dependencies["library.z"] = "file:../library.z";
- await fs.writeFile(`${this.fixturePath}/package.json`,
- JSON.stringify(packageJsonContent)
- );
- }
-
- /**
- * Helper function to add a new theme library dependency ("themelib.z") to an arbitrary root project.
- *
- * @param {string} sourceDir - source path of the root project (e.g. `${this.fixturePath}/webapp` for applications)
- */
- async addThemeLibraryDependency(sourceDir) {
- await fs.mkdir(`${this.fixturePath}/node_modules/themelib.z/src/themelib/z/themes/my_theme`, {recursive: true});
- await fs.writeFile(
- `${this.fixturePath}/node_modules/themelib.z/src/themelib/z/themes/my_theme/library.source.less`,
- `@mycolor: blue;
-.sapUiBody {
- background-color: @mycolor;
-}`);
- await fs.writeFile(`${this.fixturePath}/node_modules/themelib.z/src/themelib/z/themes/my_theme/.theme`,
- `
-
- my_theme
- me
- ` +"\"${copyright}\"" + `
- ` +"\"${version}\"" + `
-`);
- await fs.writeFile(`${this.fixturePath}/node_modules/themelib.z/ui5.yaml`,
- `---
-specVersion: "5.0"
-type: theme-library
-metadata:
- name: themelib.z
-`);
- await fs.writeFile(`${this.fixturePath}/node_modules/themelib.z/package.json`,
- `{
- "name": "themelib.z",
- "version": "1.0.0"
-}`
- );
-
- await fs.writeFile(`${sourceDir}/themelibConsumer.js`,
- `sap.ui.define(["sap/ui/core/Theming"], (Theming) => {
- Theming.setTheme("my_theme");
- console.log(Theming.getTheme());
-});`);
- const packageJsonContent = JSON.parse(
- await fs.readFile(`${this.fixturePath}/package.json`, {encoding: "utf8"}));
- if (!packageJsonContent.dependencies) {
- packageJsonContent.dependencies = {};
- }
- packageJsonContent.dependencies["themelib.z"] = "file:../themelib.z";
- await fs.writeFile(`${this.fixturePath}/package.json`,
- JSON.stringify(packageJsonContent)
- );
- }
-}
diff --git a/packages/project/test/lib/build/ProjectBuilder.raceCondition.integration.js b/packages/project/test/lib/build/ProjectBuilder.raceCondition.integration.js
new file mode 100644
index 00000000000..b358411aef4
--- /dev/null
+++ b/packages/project/test/lib/build/ProjectBuilder.raceCondition.integration.js
@@ -0,0 +1,128 @@
+import test from "ava";
+import fs from "node:fs/promises";
+import {createFixtureTesterFactory, registerBuildHooks} from "./__helper__/ProjectBuilderFixtureTester.js";
+
+const FixtureTester = createFixtureTesterFactory("race");
+registerBuildHooks(test);
+
+test.serial("Build race condition: file modified during active build", async (t) => {
+ const fixtureTester = new FixtureTester(t, "application.a");
+ const destPath = fixtureTester.destPath;
+ await fixtureTester._initialize();
+ const testFilePath = `${fixtureTester.fixturePath}/webapp/test.js`;
+ const originalContent = await fs.readFile(testFilePath, {encoding: "utf8"});
+ const addedFileName = "added-during-build.js";
+ const addedFilePath = `${fixtureTester.fixturePath}/webapp/${addedFileName}`;
+
+ // #1 Build with race condition triggered by custom task that modifies test.js during the build.
+ // The build should detect the source change and throw.
+ const error1 = await t.throwsAsync(fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-race-condition.yaml"},
+ config: {destPath, cleanDest: true},
+ }));
+ t.true(error1.message.includes("Detected changes to source files of project application.a during the build"),
+ "Error message indicates source change detected");
+
+ // #2 Revert the source file to original content
+ await fs.writeFile(testFilePath, originalContent);
+
+ // #3 Build again with normal config after reverting the source.
+ // Since the race condition build threw, no corrupted cache was written.
+ // This build should succeed and produce clean output.
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {
+ "application.a": {}
+ }
+ }
+ });
+
+ // Verify the output does NOT contain the race condition modification
+ const finalBuiltContent = await fs.readFile(`${destPath}/test.js`, {encoding: "utf8"});
+ t.false(
+ finalBuiltContent.includes(`RACE CONDITION MODIFICATION`),
+ "Build output does not contain race condition modification after clean rebuild"
+ );
+
+ // #4 Build with race condition triggered by add-file custom task
+ await fs.rm(addedFilePath, {force: true});
+ const error2 = await t.throwsAsync(fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-race-condition-add-file.yaml"},
+ config: {destPath, cleanDest: true},
+ }));
+ t.true(error2.message.includes("Detected changes to source files of project application.a during the build"),
+ "Error message indicates source change detected (add file)");
+
+ // #5 Revert source state by removing the file that was added during build
+ await fs.rm(addedFilePath, {force: true});
+
+ // #6 Build again with normal config after reverting.
+ // Cache from build #3 is still valid (same source state), so everything should be skipped.
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}
+ }
+ });
+
+ // #7 Build with race condition triggered by delete-file custom task
+ const error3 = await t.throwsAsync(fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-race-condition-delete-file.yaml"},
+ config: {destPath, cleanDest: true},
+ }));
+ t.true(error3.message.includes("Detected changes to source files of project application.a during the build"),
+ "Error message indicates source change detected (delete file)");
+
+ // #8 Revert source state by restoring the deleted file
+ await fs.writeFile(testFilePath, originalContent);
+
+ // #9 Build again with normal config after restoring.
+ // Cache from build #3 is still valid (same source state), so everything should be skipped.
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: true},
+ assertions: {
+ projects: {}
+ }
+ });
+
+ // Verify test.js is present in output
+ const restoredBuiltFileContent = await fs.readFile(`${destPath}/test.js`, {encoding: "utf8"});
+ t.true(
+ restoredBuiltFileContent.includes(`console.log`),
+ "Build output contains restored file after source recovery"
+ );
+});
+
+test.serial("Build dependency race condition: frozen source reader protects against filesystem changes",
+ async (t) => {
+ const fixtureTester = new FixtureTester(t, "application.a");
+ const destPath = fixtureTester.destPath;
+
+ // Build with dependency-race-condition custom task and all dependencies included.
+ // library.d is built first → its sources are frozen in CAS.
+ // Then application.a builds, running the custom task that:
+ // 1. Reads library.d's some.js via the dependency reader (CAS-backed)
+ // 2. Modifies some.js on disk
+ // 3. Re-reads via the dependency reader
+ // 4. Asserts the content is still the original CAS-frozen content (not modified disk)
+ // 5. Restores the file on disk
+ // If the frozen reader is not working, the custom task throws and the build fails.
+ await fixtureTester.buildProject({
+ graphConfig: {rootConfigPath: "ui5-dependency-race-condition.yaml"},
+ config: {destPath, cleanDest: true, dependencyIncludes: {includeAllDependencies: true}},
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {}
+ }
+ }
+ });
+
+ // Sanity check: verify library.d's some.js exists in build output
+ const builtContent = await fs.readFile(`${destPath}/resources/library/d/some.js`, {encoding: "utf8"});
+ t.truthy(builtContent, "library.d some.js exists in build output");
+ });
diff --git a/packages/project/test/lib/build/ProjectBuilder.versionInfo.integration.js b/packages/project/test/lib/build/ProjectBuilder.versionInfo.integration.js
new file mode 100644
index 00000000000..59d87d0fb49
--- /dev/null
+++ b/packages/project/test/lib/build/ProjectBuilder.versionInfo.integration.js
@@ -0,0 +1,120 @@
+import test from "ava";
+import fs from "node:fs/promises";
+import {createFixtureTesterFactory, registerBuildHooks} from "./__helper__/ProjectBuilderFixtureTester.js";
+
+const FixtureTester = createFixtureTesterFactory("versionInfo");
+registerBuildHooks(test);
+
+test.serial("Build with dependencies: Verify sap-ui-version.json generation and regeneration", async (t) => {
+ const fixtureTester = new FixtureTester(t, "application.a");
+ const destPath = fixtureTester.destPath;
+ const versionInfoPath = `${destPath}/resources/sap-ui-version.json`;
+
+ // Build #1: Full build with all dependencies in JSDoc mode
+ // JSDoc mode enables generateVersionInfo task which creates sap-ui-version.json
+ await fixtureTester.buildProject({
+ config: {
+ destPath,
+ cleanDest: true,
+ jsdoc: true,
+ dependencyIncludes: {includeAllDependencies: true}
+ },
+ assertions: {
+ projects: {
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {}
+ }
+ }
+ });
+
+ const versionInfo1Content = await fs.readFile(versionInfoPath, {encoding: "utf8"});
+ t.truthy(versionInfo1Content, "sap-ui-version.json should exist");
+ const versionInfo1 = JSON.parse(versionInfo1Content);
+
+ // Root project metadata
+ t.is(versionInfo1.name, "application.a", "Root project name");
+ t.is(versionInfo1.version, "1.0.0", "Root project version");
+ t.is(typeof versionInfo1.buildTimestamp, "string", "buildTimestamp is string");
+
+ // Libraries array
+ t.true(Array.isArray(versionInfo1.libraries), "libraries is array");
+ const libraryNames = versionInfo1.libraries.map((lib) => lib.name).sort();
+ t.deepEqual(libraryNames, ["library.a", "library.b", "library.c", "library.d"],
+ "Contains all dependency libraries");
+
+ // Each library has required fields
+ versionInfo1.libraries.forEach((lib) => {
+ t.is(typeof lib.name, "string", `Library ${lib.name} has name`);
+ t.is(typeof lib.version, "string", `Library ${lib.name} has version`);
+ t.is(typeof lib.buildTimestamp, "string", `Library ${lib.name} has buildTimestamp`);
+ });
+
+ const firstBuildTimestamp = versionInfo1.buildTimestamp;
+
+ // Build #2: No changes, expect full cache hit
+ await fixtureTester.buildProject({
+ config: {
+ destPath,
+ cleanDest: true,
+ jsdoc: true,
+ dependencyIncludes: {includeAllDependencies: true}
+ },
+ assertions: {
+ projects: {} // All projects cached
+ }
+ });
+
+ // Verify sap-ui-version.json was reused from cache (timestamp unchanged)
+ const versionInfo2Content = await fs.readFile(versionInfoPath, {encoding: "utf8"});
+ const versionInfo2 = JSON.parse(versionInfo2Content);
+ t.is(versionInfo2.buildTimestamp, firstBuildTimestamp,
+ "buildTimestamp unchanged when cached (no source changes)");
+});
+
+test.serial("Build application.a with --exclude-task=generateVersionInfo", async (t) => {
+ // This test verifies that when task generateVersionInfo is excluded,
+ // the sap-ui-version.json file is NOT generated in the output
+ // AND the dependencies are not automatically built.
+
+ const fixtureTester = new FixtureTester(t, "application.a");
+ const destPath = fixtureTester.destPath;
+
+ // #1 Build with empty cache and exclude generateVersionInfo task
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: false, excludedTasks: ["generateVersionInfo"]},
+ assertions: {
+ projects: {
+ // Only application.a is built, dependencies are skipped
+ "application.a": {}
+ }
+ }
+ });
+
+ // Verify that sap-ui-version.json does NOT exist in the output
+ const versionInfoPath = `${destPath}/resources/sap-ui-version.json`;
+ await t.throwsAsync(fs.readFile(versionInfoPath, {encoding: "utf8"}), undefined,
+ "sap-ui-version.json should NOT exist when generateVersionInfo task is excluded");
+
+
+ // #2 Build with empty cache and include all default tasks now
+ await fixtureTester.buildProject({
+ config: {destPath, cleanDest: false},
+ assertions: {
+ projects: {
+ // All dependencies are built now
+ "library.d": {},
+ "library.a": {},
+ "library.b": {},
+ "library.c": {},
+ "application.a": {}
+ }
+ }
+ });
+
+ // Verify that sap-ui-version.json DOES exist in the output now
+ await t.notThrowsAsync(fs.readFile(versionInfoPath, {encoding: "utf8"}), undefined,
+ "sap-ui-version.json should exist when generateVersionInfo task is included");
+});
diff --git a/packages/project/test/lib/build/__helper__/ProjectBuilderFixtureTester.js b/packages/project/test/lib/build/__helper__/ProjectBuilderFixtureTester.js
new file mode 100644
index 00000000000..d0402a38e23
--- /dev/null
+++ b/packages/project/test/lib/build/__helper__/ProjectBuilderFixtureTester.js
@@ -0,0 +1,409 @@
+import sinonGlobal from "sinon";
+import {fileURLToPath} from "node:url";
+import fs from "node:fs/promises";
+import {graphFromPackageDependencies} from "../../../../lib/graph/graph.js";
+import {setLogLevel} from "@ui5/logger";
+
+// Ensures that all logging code paths are tested
+setLogLevel("silly");
+
+/**
+ * Registers the AVA beforeEach/afterEach hooks that wire up the ui5.* process event stubs.
+ * Each test file calls this with its own AVA `test` object so the hooks register against the
+ * file's own test instance.
+ *
+ * @param {import("ava").TestFn} test The AVA test object of the calling test file
+ */
+export function registerBuildHooks(test) {
+ test.beforeEach((t) => {
+ const sinon = t.context.sinon = sinonGlobal.createSandbox();
+
+ t.context.logEventStub = sinon.stub();
+ t.context.buildMetadataEventStub = sinon.stub();
+ t.context.projectBuildMetadataEventStub = sinon.stub();
+ t.context.buildStatusEventStub = sinon.stub();
+ t.context.projectBuildStatusEventStub = sinon.stub();
+
+ process.on("ui5.log", t.context.logEventStub);
+ process.on("ui5.build-metadata", t.context.buildMetadataEventStub);
+ process.on("ui5.project-build-metadata", t.context.projectBuildMetadataEventStub);
+ process.on("ui5.build-status", t.context.buildStatusEventStub);
+ process.on("ui5.project-build-status", t.context.projectBuildStatusEventStub);
+ });
+
+ test.afterEach.always((t) => {
+ t.context.sinon.restore();
+
+ process.off("ui5.log", t.context.logEventStub);
+ process.off("ui5.build-metadata", t.context.buildMetadataEventStub);
+ process.off("ui5.project-build-metadata", t.context.projectBuildMetadataEventStub);
+ process.off("ui5.build-status", t.context.buildStatusEventStub);
+ process.off("ui5.project-build-status", t.context.projectBuildStatusEventStub);
+ });
+}
+
+/**
+ * Creates a FixtureTester subclass bound to the given scope. The scope namespaces the
+ * temporary directory tree per test file so that fixtures shared across files
+ * (e.g. "application.a") don't collide when AVA runs the files in parallel.
+ *
+ * The returned class is used as `new FixtureTester(t, fixtureName)` in each test file.
+ *
+ * @param {string} scope Unique scope segment for the calling test file (e.g. "bundling")
+ * @returns {typeof FixtureTester} A FixtureTester subclass bound to `scope`
+ */
+export function createFixtureTesterFactory(scope) {
+ return class ScopedFixtureTester extends FixtureTester {
+ constructor(t, fixtureName) {
+ super(t, fixtureName, scope);
+ }
+ };
+}
+
+function getFixturePath(fixtureName) {
+ return fileURLToPath(new URL(`../../../fixtures/${fixtureName}`, import.meta.url));
+}
+
+function getTmpPath(folderName) {
+ return fileURLToPath(new URL(`../../../tmp/ProjectBuilder/${folderName}`, import.meta.url));
+}
+
+async function rmrf(dirPath) {
+ return fs.rm(dirPath, {recursive: true, force: true, maxRetries: 3, retryDelay: 200});
+}
+
+class FixtureTester {
+ constructor(t, fixtureName, scope) {
+ this._t = t;
+ this._sinon = t.context.sinon;
+ this._fixtureName = fixtureName;
+ this._initialized = false;
+
+ // Public
+ this.fixturePath = getTmpPath(`${scope}/${fixtureName}`);
+ this.destPath = getTmpPath(`${scope}/${fixtureName}/dist`);
+ this.ui5DataDir = getTmpPath(`${scope}/${fixtureName}/.ui5`);
+ }
+
+ async _initialize() {
+ if (this._initialized) {
+ return;
+ }
+ await rmrf(this.fixturePath); // Clean up any previous test runs
+ await fs.cp(getFixturePath(this._fixtureName), this.fixturePath, {recursive: true});
+ this._initialized = true;
+ }
+
+ async buildProject({graphConfig = {}, config = {}, assertions} = {}) {
+ await this._initialize();
+ this._sinon.resetHistory();
+
+ const graph = await graphFromPackageDependencies({
+ ...graphConfig,
+ cwd: this.fixturePath,
+ });
+
+ // Execute the build
+ await graph.build({...config, ui5DataDir: this.ui5DataDir});
+
+ // Apply assertions if provided
+ if (assertions) {
+ this._assertBuild(assertions);
+ }
+ }
+
+ _assertBuild(assertions) {
+ /**
+ * assertions object structure:
+ * {
+ * projects: {
+ * "projectName": {
+ * skippedTasks: ["task1", "task2"],
+ * },
+ * // ...
+ * },
+ * allProjects: ["projectName1", "projectName2"]
+ * }
+ *
+ * projects - for asserting all projects which are expected to be built
+ * allProjects - optional, for asserting all seen projects nonetheless if built or not
+ */
+ const {projects = {}, allProjects = []} = assertions;
+
+ const projectsInOrder = [];
+ const seenProjects = new Set();
+ const tasksByProject = {};
+
+ // Extract build status to identify built projects and their order
+ const buildStatusEvents = this._t.context.buildStatusEventStub.args.map((args) => args[0]);
+ for (const event of buildStatusEvents) {
+ if (!seenProjects.has(event.projectName)) {
+ seenProjects.add(event.projectName);
+ if (event.status === "project-build-start") {
+ projectsInOrder.push(event.projectName);
+ }
+ }
+ }
+
+ // Extract task status to identify skipped & executed tasks per project
+ const projectBuildStatusEvents = this._t.context.projectBuildStatusEventStub.args.map((args) => args[0]);
+ for (const event of projectBuildStatusEvents) {
+ if (!tasksByProject[event.projectName]) {
+ tasksByProject[event.projectName] = {executed: [], skipped: []};
+ }
+ if (event.status === "task-skip") {
+ tasksByProject[event.projectName].skipped.push(event.taskName);
+ } else if (event.status === "task-start") {
+ tasksByProject[event.projectName].executed.push(event.taskName);
+ }
+ }
+
+ // Assert built projects in order
+ const expectedProjects = Object.keys(projects);
+ this._t.deepEqual(projectsInOrder, expectedProjects);
+
+ // Optional check: Assert seen projects
+ if (allProjects.length > 0) {
+ const expectedAllProjects = allProjects.sort();
+ const actualAllProjects = Array.from(seenProjects).sort();
+ this._t.deepEqual(actualAllProjects, expectedAllProjects,
+ "All seen projects (built or not) should match expected");
+ }
+
+ // Assert skipped tasks per project
+ for (const [projectName, expectedSkipped] of Object.entries(projects)) {
+ const skippedTasks = expectedSkipped.skippedTasks || [];
+ const actualSkipped = (tasksByProject[projectName]?.skipped || []).sort();
+ const expectedArray = skippedTasks.sort();
+ this._t.deepEqual(actualSkipped, expectedArray);
+ }
+ }
+
+ /**
+ * Helper function to add a new module dependency ("module.z") to an arbitrary root project.
+ *
+ * @param {string} sourceDir - source path of the root project (e.g. `${this.fixturePath}/webapp` for applications)
+ */
+ async addModuleDependency(sourceDir) {
+ await fs.mkdir(`${this.fixturePath}/node_modules/module.z/dev`, {recursive: true});
+ await fs.writeFile(`${this.fixturePath}/node_modules/module.z/dev/devTools.js`,
+ `console.log("module.z devTools");`);
+ await fs.writeFile(`${this.fixturePath}/node_modules/module.z/package.json`,
+ `{
+ "name": "module.z",
+ "version": "1.0.0"
+}`
+ );
+ await fs.writeFile(`${this.fixturePath}/node_modules/module.z/ui5.yaml`,
+ `---
+specVersion: "5.0"
+type: module
+metadata:
+ name: module.z
+resources:
+ configuration:
+ paths:
+ /resources/z/module/dev/: dev`);
+
+ await fs.writeFile(`${sourceDir}/moduleConsumer.js`,
+ `sap.ui.define(["z/module/dev/devTools"], () => {});`);
+ const packageJsonContent = JSON.parse(
+ await fs.readFile(`${this.fixturePath}/package.json`, {encoding: "utf8"}));
+ if (!packageJsonContent.dependencies) {
+ packageJsonContent.dependencies = {};
+ }
+ packageJsonContent.dependencies["module.z"] = "file:../module.z";
+ await fs.writeFile(`${this.fixturePath}/package.json`,
+ JSON.stringify(packageJsonContent)
+ );
+ }
+
+ /**
+ * Helper function to add a new component dependency ("component.z") to an arbitrary root project.
+ *
+ * @param {string} sourceDir - source path of the root project (e.g. `${this.fixturePath}/webapp` for applications)
+ */
+ async addComponentDependency(sourceDir) {
+ await fs.mkdir(`${this.fixturePath}/node_modules/component.z/src`, {recursive: true});
+ await fs.writeFile(`${this.fixturePath}/node_modules/component.z/src/Component.js`,
+ `sap.ui.define(["sap/ui/core/UIComponent"], function(UIComponent){
+ "use strict";
+ return UIComponent.extend('component.z.Component', {
+ createContent: function () {
+ return new Label({ text: "Hello!" });
+ }
+ });
+});
+`);
+ await fs.writeFile(`${this.fixturePath}/node_modules/component.z/src/manifest.json`,
+ `{
+ "_version": "1.1.0",
+ "sap.app": {
+ "_version": "1.1.0",
+ "id": "component.z",
+ "type": "component",
+ "applicationVersion": {
+ "version": "1.2.2"
+ },
+ "embeds": ["embedded"],
+ "title": "{{title}}"
+ }
+}`);
+ await fs.writeFile(`${this.fixturePath}/node_modules/component.z/ui5.yaml`,
+ `---
+specVersion: "5.0"
+type: component
+metadata:
+ name: component.z`);
+ await fs.writeFile(`${this.fixturePath}/node_modules/component.z/package.json`,
+ `{
+ "name": "component.z",
+ "version": "1.0.0"
+}`
+ );
+
+ await fs.writeFile(`${sourceDir}/componentConsumer.js`,
+ `sap.ui.define(["component/z"], () => {});`);
+ const packageJsonContent = JSON.parse(
+ await fs.readFile(`${this.fixturePath}/package.json`, {encoding: "utf8"}));
+ if (!packageJsonContent.dependencies) {
+ packageJsonContent.dependencies = {};
+ }
+ packageJsonContent.dependencies["component.z"] = "file:../component.z";
+ await fs.writeFile(`${this.fixturePath}/package.json`,
+ JSON.stringify(packageJsonContent)
+ );
+ }
+
+ /**
+ * Helper function to add a new library dependency ("library.z") to an arbitrary root project.
+ *
+ * @param {string} sourceDir - source path of the root project (e.g. `${this.fixturePath}/webapp` for applications)
+ */
+ async addLibraryDependency(sourceDir) {
+ await fs.mkdir(`${this.fixturePath}/node_modules/library.z/src/library/z`, {recursive: true});
+ await fs.writeFile(`${this.fixturePath}/node_modules/library.z/src/library/z/library.js`,
+ `
+sap.ui.define([
+ "sap/base/util/ObjectPath",
+ "sap/ui/core/Core",
+ "sap/ui/core/library"
+], function (ObjectPath, Core) {
+ "use strict";
+
+ Core.initLibrary({
+ name: "library.z",
+ version: ` + "\"${version}\"" + `,
+ dependencies: [
+ "sap.ui.core"
+ ],
+ types: [
+ "library.z.ExampleColor"
+ ],
+ interfaces: [],
+ elements: [],
+ noLibraryCSS: false
+ });
+ const thisLib = ObjectPath.get("library.z");
+
+ thisLib.ExampleColor = {
+ Default : "Default",
+ Highlight : "Highlight"
+ };
+ return thisLib;
+});`);
+ await fs.writeFile(`${this.fixturePath}/node_modules/library.z/src/library/z/manifest.json`,
+ JSON.stringify({"sap.app": {"id": "library.z", "type": "library"}}, null, "\t"));
+ await fs.writeFile(`${this.fixturePath}/node_modules/library.z/src/library/z/.library`,
+ `
+
+
+ library.z
+ SAP SE
+ Some fancy copyright
+ `+"${version}"+`
+
+ Library Z
+
+`);
+ await fs.writeFile(`${this.fixturePath}/node_modules/library.z/ui5.yaml`,
+ `---
+specVersion: "5.0"
+type: library
+metadata:
+ name: library.z
+`);
+ await fs.writeFile(`${this.fixturePath}/node_modules/library.z/package.json`,
+ `{
+ "name": "library.z",
+ "version": "1.0.0"
+}`
+ );
+
+ await fs.writeFile(`${sourceDir}/libraryConsumer.js`,
+ `sap.ui.define(["library/z/library"],
+ (LibraryZ) => {
+ console.log(LibraryZ.ExampleColor.Default);
+});`);
+ const packageJsonContent = JSON.parse(
+ await fs.readFile(`${this.fixturePath}/package.json`, {encoding: "utf8"}));
+ if (!packageJsonContent.dependencies) {
+ packageJsonContent.dependencies = {};
+ }
+ packageJsonContent.dependencies["library.z"] = "file:../library.z";
+ await fs.writeFile(`${this.fixturePath}/package.json`,
+ JSON.stringify(packageJsonContent)
+ );
+ }
+
+ /**
+ * Helper function to add a new theme library dependency ("themelib.z") to an arbitrary root project.
+ *
+ * @param {string} sourceDir - source path of the root project (e.g. `${this.fixturePath}/webapp` for applications)
+ */
+ async addThemeLibraryDependency(sourceDir) {
+ await fs.mkdir(`${this.fixturePath}/node_modules/themelib.z/src/themelib/z/themes/my_theme`, {recursive: true});
+ await fs.writeFile(
+ `${this.fixturePath}/node_modules/themelib.z/src/themelib/z/themes/my_theme/library.source.less`,
+ `@mycolor: blue;
+.sapUiBody {
+ background-color: @mycolor;
+}`);
+ await fs.writeFile(`${this.fixturePath}/node_modules/themelib.z/src/themelib/z/themes/my_theme/.theme`,
+ `
+
+ my_theme
+ me
+ ` +"\"${copyright}\"" + `
+ ` +"\"${version}\"" + `
+`);
+ await fs.writeFile(`${this.fixturePath}/node_modules/themelib.z/ui5.yaml`,
+ `---
+specVersion: "5.0"
+type: theme-library
+metadata:
+ name: themelib.z
+`);
+ await fs.writeFile(`${this.fixturePath}/node_modules/themelib.z/package.json`,
+ `{
+ "name": "themelib.z",
+ "version": "1.0.0"
+}`
+ );
+
+ await fs.writeFile(`${sourceDir}/themelibConsumer.js`,
+ `sap.ui.define(["sap/ui/core/Theming"], (Theming) => {
+ Theming.setTheme("my_theme");
+ console.log(Theming.getTheme());
+});`);
+ const packageJsonContent = JSON.parse(
+ await fs.readFile(`${this.fixturePath}/package.json`, {encoding: "utf8"}));
+ if (!packageJsonContent.dependencies) {
+ packageJsonContent.dependencies = {};
+ }
+ packageJsonContent.dependencies["themelib.z"] = "file:../themelib.z";
+ await fs.writeFile(`${this.fixturePath}/package.json`,
+ JSON.stringify(packageJsonContent)
+ );
+ }
+}