diff --git a/packages/builder/lib/lbt/bundle/Resolver.js b/packages/builder/lib/lbt/bundle/Resolver.js
index fdd9c95b622..4f90bd6d7aa 100644
--- a/packages/builder/lib/lbt/bundle/Resolver.js
+++ b/packages/builder/lib/lbt/bundle/Resolver.js
@@ -329,6 +329,15 @@ class BundleResolver {
return collectModulesForSection(section).
then( (modules) => {
if ( section.mode == SectionType.Raw && section.sort !== false ) {
+ // Sort modules by their filter definition order to ensure
+ // deterministic input for the topological sort.
+ // Topological sort preserves input order for tie-breaking,
+ // so this ensures modules at the same dependency level
+ // appear in the order defined by the section filters.
+ const filterOrder = new ResourceFilterList(section.filters, fileTypes);
+ modules.sort((a, b) => {
+ return filterOrder.matchIndex(a) - filterOrder.matchIndex(b);
+ });
// sort the modules in topological order
return topologicalSort(pool, modules).then( (modules) => {
log.silly(` Resolved modules (sorted): ${modules}`);
diff --git a/packages/builder/lib/lbt/resources/ResourceFilterList.js b/packages/builder/lib/lbt/resources/ResourceFilterList.js
index d45fc4ad649..9df4bdd2e13 100644
--- a/packages/builder/lib/lbt/resources/ResourceFilterList.js
+++ b/packages/builder/lib/lbt/resources/ResourceFilterList.js
@@ -124,6 +124,16 @@ export default class ResourceFilterList {
);
}
+ matchIndex(candidate) {
+ for (let i = 0; i < this.matchers.length; i++) {
+ const matcher = this.matchers[i];
+ if (matcher.include && matcher.calcMatch(candidate, false)) {
+ return i;
+ }
+ }
+ return this.matchers.length;
+ }
+
toString() {
return this.matchers.map((matcher) => matcher.pattern).join(",");
}
diff --git a/packages/builder/test/lib/lbt/bundle/Builder.js b/packages/builder/test/lib/lbt/bundle/Builder.js
index 082c425d838..db6dd1391e8 100644
--- a/packages/builder/test/lib/lbt/bundle/Builder.js
+++ b/packages/builder/test/lib/lbt/bundle/Builder.js
@@ -3064,6 +3064,158 @@ ${SOURCE_MAPPING_URL}=Component-preload.js.map
"bundle info subModules are correct");
});
+test("integration: createBundle raw section with deterministic ordering", async (t) => {
+ const pool = new ResourcePool();
+ pool.addResource({
+ name: "raw-module1.js",
+ getPath: () => "raw-module1.js",
+ string: function() {
+ return this.buffer();
+ },
+ buffer: async () => "// raw-module1"
+ });
+ pool.addResource({
+ name: "raw-module2.js",
+ getPath: () => "raw-module2.js",
+ string: function() {
+ return this.buffer();
+ },
+ buffer: async () => "// raw-module2"
+ });
+ pool.addResource({
+ name: "raw-dependency.js",
+ getPath: () => "raw-dependency.js",
+ string: function() {
+ return this.buffer();
+ },
+ buffer: async () => "// raw-dependency"
+ });
+ await pool.addResource({
+ name: "a.library",
+ getPath: () => "a.library",
+ string: function() {
+ return this.buffer();
+ },
+ buffer: async () => `
+
+
+
+
+
+
+
+
+
+
+`
+ });
+
+ const bundleDefinition = {
+ name: `bundle.js`,
+ defaultFileTypes: [".js"],
+ sections: [{
+ mode: "raw",
+ filters: ["raw-module2.js", "raw-module1.js", "raw-dependency.js"],
+ sort: true
+ }]
+ };
+
+ const builder = new Builder(pool);
+ const oResult = await builder.createBundle(bundleDefinition, {numberOfParts: 1, decorateBootstrapModule: false});
+ t.is(oResult.name, "bundle.js");
+ const expectedContent = `//@ui5-bundle bundle.js
+//@ui5-bundle-raw-include raw-dependency.js
+// raw-dependency
+//@ui5-bundle-raw-include raw-module2.js
+// raw-module2
+//@ui5-bundle-raw-include raw-module1.js
+// raw-module1
+${SOURCE_MAPPING_URL}=bundle.js.map
+`;
+ t.deepEqual(oResult.content, expectedContent,
+ "Raw modules should be ordered deterministically: " +
+ "dependency first, then remaining modules in filter definition order");
+ t.deepEqual(oResult.bundleInfo.subModules,
+ ["raw-dependency.js", "raw-module2.js", "raw-module1.js"],
+ "bundle info subModules reflect the deterministic order");
+});
+
+test("integration: createBundle raw section with deterministic ordering (glob filters)", async (t) => {
+ const pool = new ResourcePool();
+ pool.addResource({
+ name: "vendor/mod-a.js",
+ getPath: () => "vendor/mod-a.js",
+ string: function() {
+ return this.buffer();
+ },
+ buffer: async () => "// mod-a"
+ });
+ pool.addResource({
+ name: "vendor/mod-b.js",
+ getPath: () => "vendor/mod-b.js",
+ string: function() {
+ return this.buffer();
+ },
+ buffer: async () => "// mod-b"
+ });
+ pool.addResource({
+ name: "vendor/dep.js",
+ getPath: () => "vendor/dep.js",
+ string: function() {
+ return this.buffer();
+ },
+ buffer: async () => "// dep"
+ });
+ await pool.addResource({
+ name: "a.library",
+ getPath: () => "a.library",
+ string: function() {
+ return this.buffer();
+ },
+ buffer: async () => `
+
+
+
+
+
+
+
+
+
+
+`
+ });
+
+ const bundleDefinition = {
+ name: `bundle.js`,
+ defaultFileTypes: [".js"],
+ sections: [{
+ mode: "raw",
+ filters: ["vendor/mod-b.js", "vendor/*"],
+ sort: true
+ }]
+ };
+
+ const builder = new Builder(pool);
+ const oResult = await builder.createBundle(bundleDefinition, {numberOfParts: 1, decorateBootstrapModule: false});
+ t.is(oResult.name, "bundle.js");
+ const expectedContent = `//@ui5-bundle bundle.js
+//@ui5-bundle-raw-include vendor/dep.js
+// dep
+//@ui5-bundle-raw-include vendor/mod-b.js
+// mod-b
+//@ui5-bundle-raw-include vendor/mod-a.js
+// mod-a
+${SOURCE_MAPPING_URL}=bundle.js.map
+`;
+ t.deepEqual(oResult.content, expectedContent,
+ "Raw modules should be ordered deterministically with glob filters: " +
+ "dependency first, then mod-b (explicit filter, index 0) before mod-a (glob match, index 1)");
+ t.deepEqual(oResult.bundleInfo.subModules,
+ ["vendor/dep.js", "vendor/mod-b.js", "vendor/mod-a.js"],
+ "bundle info subModules reflect the deterministic order");
+});
+
test.serial("getEffectiveUi5MajorVersion without cache", async (t) => {
const pool = new ResourcePool();
pool.addResource({