Skip to content

Commit 9d19923

Browse files
matz3RandomByte
authored andcommitted
fix(builder): Update lbt/bundle/Resolver to ensure deterministic ordering of raw modules
When multiple raw modules share a common dependency, the topological sort tie-breaks by input order. Previously, the input order depended on async resolution timing (pool insertion order), making the output non-deterministic. Now modules are sorted by their filter definition order before topological sorting, ensuring that modules at the same dependency level appear in the order defined by the section's filter list.
1 parent 5d1dcb7 commit 9d19923

3 files changed

Lines changed: 171 additions & 0 deletions

File tree

packages/builder/lib/lbt/bundle/Resolver.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,15 @@ class BundleResolver {
329329
return collectModulesForSection(section).
330330
then( (modules) => {
331331
if ( section.mode == SectionType.Raw && section.sort !== false ) {
332+
// Sort modules by their filter definition order to ensure
333+
// deterministic input for the topological sort.
334+
// Topological sort preserves input order for tie-breaking,
335+
// so this ensures modules at the same dependency level
336+
// appear in the order defined by the section filters.
337+
const filterOrder = new ResourceFilterList(section.filters, fileTypes);
338+
modules.sort((a, b) => {
339+
return filterOrder.matchIndex(a) - filterOrder.matchIndex(b);
340+
});
332341
// sort the modules in topological order
333342
return topologicalSort(pool, modules).then( (modules) => {
334343
log.silly(` Resolved modules (sorted): ${modules}`);

packages/builder/lib/lbt/resources/ResourceFilterList.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,16 @@ export default class ResourceFilterList {
124124
);
125125
}
126126

127+
matchIndex(candidate) {
128+
for (let i = 0; i < this.matchers.length; i++) {
129+
const matcher = this.matchers[i];
130+
if (matcher.include && matcher.calcMatch(candidate, false)) {
131+
return i;
132+
}
133+
}
134+
return this.matchers.length;
135+
}
136+
127137
toString() {
128138
return this.matchers.map((matcher) => matcher.pattern).join(",");
129139
}

packages/builder/test/lib/lbt/bundle/Builder.js

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3064,6 +3064,158 @@ ${SOURCE_MAPPING_URL}=Component-preload.js.map
30643064
"bundle info subModules are correct");
30653065
});
30663066

3067+
test("integration: createBundle raw section with deterministic ordering", async (t) => {
3068+
const pool = new ResourcePool();
3069+
pool.addResource({
3070+
name: "raw-module1.js",
3071+
getPath: () => "raw-module1.js",
3072+
string: function() {
3073+
return this.buffer();
3074+
},
3075+
buffer: async () => "// raw-module1"
3076+
});
3077+
pool.addResource({
3078+
name: "raw-module2.js",
3079+
getPath: () => "raw-module2.js",
3080+
string: function() {
3081+
return this.buffer();
3082+
},
3083+
buffer: async () => "// raw-module2"
3084+
});
3085+
pool.addResource({
3086+
name: "raw-dependency.js",
3087+
getPath: () => "raw-dependency.js",
3088+
string: function() {
3089+
return this.buffer();
3090+
},
3091+
buffer: async () => "// raw-dependency"
3092+
});
3093+
await pool.addResource({
3094+
name: "a.library",
3095+
getPath: () => "a.library",
3096+
string: function() {
3097+
return this.buffer();
3098+
},
3099+
buffer: async () => `<?xml version="1.0" encoding="UTF-8" ?>
3100+
<library xmlns="http://www.sap.com/sap.ui.library.xsd" >
3101+
<appData>
3102+
<packaging xmlns="http://www.sap.com/ui5/buildext/packaging" version="2.0" >
3103+
<module-infos>
3104+
<raw-module name="raw-module1.js" depends="raw-dependency.js" />
3105+
<raw-module name="raw-module2.js" depends="raw-dependency.js" />
3106+
<raw-module name="raw-dependency.js" />
3107+
</module-infos>
3108+
</packaging>
3109+
</appData>
3110+
</library>`
3111+
});
3112+
3113+
const bundleDefinition = {
3114+
name: `bundle.js`,
3115+
defaultFileTypes: [".js"],
3116+
sections: [{
3117+
mode: "raw",
3118+
filters: ["raw-module2.js", "raw-module1.js", "raw-dependency.js"],
3119+
sort: true
3120+
}]
3121+
};
3122+
3123+
const builder = new Builder(pool);
3124+
const oResult = await builder.createBundle(bundleDefinition, {numberOfParts: 1, decorateBootstrapModule: false});
3125+
t.is(oResult.name, "bundle.js");
3126+
const expectedContent = `//@ui5-bundle bundle.js
3127+
//@ui5-bundle-raw-include raw-dependency.js
3128+
// raw-dependency
3129+
//@ui5-bundle-raw-include raw-module2.js
3130+
// raw-module2
3131+
//@ui5-bundle-raw-include raw-module1.js
3132+
// raw-module1
3133+
${SOURCE_MAPPING_URL}=bundle.js.map
3134+
`;
3135+
t.deepEqual(oResult.content, expectedContent,
3136+
"Raw modules should be ordered deterministically: " +
3137+
"dependency first, then remaining modules in filter definition order");
3138+
t.deepEqual(oResult.bundleInfo.subModules,
3139+
["raw-dependency.js", "raw-module2.js", "raw-module1.js"],
3140+
"bundle info subModules reflect the deterministic order");
3141+
});
3142+
3143+
test("integration: createBundle raw section with deterministic ordering (glob filters)", async (t) => {
3144+
const pool = new ResourcePool();
3145+
pool.addResource({
3146+
name: "vendor/mod-a.js",
3147+
getPath: () => "vendor/mod-a.js",
3148+
string: function() {
3149+
return this.buffer();
3150+
},
3151+
buffer: async () => "// mod-a"
3152+
});
3153+
pool.addResource({
3154+
name: "vendor/mod-b.js",
3155+
getPath: () => "vendor/mod-b.js",
3156+
string: function() {
3157+
return this.buffer();
3158+
},
3159+
buffer: async () => "// mod-b"
3160+
});
3161+
pool.addResource({
3162+
name: "vendor/dep.js",
3163+
getPath: () => "vendor/dep.js",
3164+
string: function() {
3165+
return this.buffer();
3166+
},
3167+
buffer: async () => "// dep"
3168+
});
3169+
await pool.addResource({
3170+
name: "a.library",
3171+
getPath: () => "a.library",
3172+
string: function() {
3173+
return this.buffer();
3174+
},
3175+
buffer: async () => `<?xml version="1.0" encoding="UTF-8" ?>
3176+
<library xmlns="http://www.sap.com/sap.ui.library.xsd" >
3177+
<appData>
3178+
<packaging xmlns="http://www.sap.com/ui5/buildext/packaging" version="2.0" >
3179+
<module-infos>
3180+
<raw-module name="vendor/mod-a.js" depends="vendor/dep.js" />
3181+
<raw-module name="vendor/mod-b.js" depends="vendor/dep.js" />
3182+
<raw-module name="vendor/dep.js" />
3183+
</module-infos>
3184+
</packaging>
3185+
</appData>
3186+
</library>`
3187+
});
3188+
3189+
const bundleDefinition = {
3190+
name: `bundle.js`,
3191+
defaultFileTypes: [".js"],
3192+
sections: [{
3193+
mode: "raw",
3194+
filters: ["vendor/mod-b.js", "vendor/*"],
3195+
sort: true
3196+
}]
3197+
};
3198+
3199+
const builder = new Builder(pool);
3200+
const oResult = await builder.createBundle(bundleDefinition, {numberOfParts: 1, decorateBootstrapModule: false});
3201+
t.is(oResult.name, "bundle.js");
3202+
const expectedContent = `//@ui5-bundle bundle.js
3203+
//@ui5-bundle-raw-include vendor/dep.js
3204+
// dep
3205+
//@ui5-bundle-raw-include vendor/mod-b.js
3206+
// mod-b
3207+
//@ui5-bundle-raw-include vendor/mod-a.js
3208+
// mod-a
3209+
${SOURCE_MAPPING_URL}=bundle.js.map
3210+
`;
3211+
t.deepEqual(oResult.content, expectedContent,
3212+
"Raw modules should be ordered deterministically with glob filters: " +
3213+
"dependency first, then mod-b (explicit filter, index 0) before mod-a (glob match, index 1)");
3214+
t.deepEqual(oResult.bundleInfo.subModules,
3215+
["vendor/dep.js", "vendor/mod-b.js", "vendor/mod-a.js"],
3216+
"bundle info subModules reflect the deterministic order");
3217+
});
3218+
30673219
test.serial("getEffectiveUi5MajorVersion without cache", async (t) => {
30683220
const pool = new ResourcePool();
30693221
pool.addResource({

0 commit comments

Comments
 (0)