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({