Skip to content

Commit f94939d

Browse files
committed
[BREAKING] Discontinue bundling of JavaScript modules as string
The UI5 bundler packages JavaScript files that are identified as "requiresTopLevelScope" as a string, to be evaluated via "eval" at runtime. This behavior ensures that the script works as expected, e.g. with regards to implicit globals. This "eval" runtime feature with be discontinued in UI5 2.0 because of security best practices (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval) and to comply with stricter CSP settings (unsafe-eval). By using specVersion 4.0 an error is thrown when bundling a module as string and the module is not included in the bundle at all. JIRA: CPOUI5FOUNDATION-794
1 parent 4b2f341 commit f94939d

File tree

61 files changed

+1298
-137
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+1298
-137
lines changed

lib/lbt/bundle/Builder.js

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,13 @@ function isEmptyBundle(resolvedBundle) {
4545
}
4646

4747
class BundleBuilder {
48-
constructor(pool, targetUi5CoreVersion) {
48+
constructor(pool, targetUi5CoreVersion, allowStringBundling) {
4949
this.pool = pool;
5050
this.resolver = new BundleResolver(pool);
5151
this.splitter = new BundleSplitter(pool, this.resolver);
5252
this.targetUi5CoreVersion = targetUi5CoreVersion;
5353
this.targetUi5CoreVersionMajor = undefined;
54+
this.allowStringBundling = allowStringBundling;
5455
}
5556

5657
getUi5MajorVersion() {
@@ -172,7 +173,7 @@ class BundleBuilder {
172173

173174
this.closeModule(resolvedModule);
174175

175-
const bundleInfo = await resolvedModule.createModuleInfo(this.pool);
176+
const bundleInfo = await resolvedModule.createModuleInfo(this.pool, this.allowStringBundling);
176177
bundleInfo.size = this.outW.length;
177178

178179
return {
@@ -231,23 +232,23 @@ class BundleBuilder {
231232
}
232233
}
233234

234-
addSection(section) {
235+
async addSection(section) {
235236
this.ensureRawDeclarations();
236237

237238
switch (section.mode) {
238239
case SectionType.Provided:
239240
// do nothing
240241
return undefined; // nothing to wait for
241242
case SectionType.Raw:
242-
return this.writeRaw(section);
243+
return await this.writeRaw(section);
243244
case SectionType.Preload:
244-
return this.writePreloadFunction(section);
245+
return await this.writePreloadFunction(section);
245246
case SectionType.BundleInfo:
246-
return this.writeBundleInfos([section]);
247+
return await this.writeBundleInfos([section]);
247248
case SectionType.Require:
248-
return this.writeRequires(section);
249+
return await this.writeRequires(section);
249250
case SectionType.DepCache:
250-
return this.writeDepCache(section);
251+
return await this.writeDepCache(section);
251252
default:
252253
throw new Error("unknown section mode " + section.mode);
253254
}
@@ -330,7 +331,6 @@ class BundleBuilder {
330331
if ( i>0 ) {
331332
outW.writeln(",");
332333
}
333-
// this.beforeWritePreloadModule(module, resource.info, resource);
334334
outW.write(`\t"${module.toString()}":`);
335335
outW.startSegment(module);
336336
await this.writePreloadModule(module, resource.info, resource);
@@ -397,8 +397,13 @@ class BundleBuilder {
397397
const remaining = [];
398398
for ( const moduleName of sequence ) {
399399
if ( /\.js$/.test(moduleName) ) {
400-
// console.log("Processing " + moduleName);
401400
const resource = await this.pool.findResourceWithInfo(moduleName);
401+
402+
if (resource.info?.requiresTopLevelScope && !this.allowStringBundling) {
403+
this.logStringBundlingError(moduleName);
404+
continue;
405+
}
406+
402407
let moduleContent = (await resource.buffer()).toString();
403408
moduleContent = removeHashbang(moduleContent);
404409
let moduleSourceMap;
@@ -478,8 +483,8 @@ class BundleBuilder {
478483
if (this.options.sourceMap) {
479484
// We are actually not interested in the source map this module might contain,
480485
// but we should make sure to remove any "sourceMappingURL" from the module content before
481-
// writing it to the bundle. Otherwise browser dev-tools might create unnecessary (and likely incorrect)
482-
// requests for any referenced .map files
486+
// writing it to the bundle. Otherwise browser dev-tools might create unnecessary
487+
// (and likely incorrect) requests for any referenced .map files
483488
({moduleContent} =
484489
await this.getSourceMapForModule({
485490
moduleName,
@@ -542,15 +547,41 @@ class BundleBuilder {
542547
});
543548
}
544549

545-
writeBundleInfos(sections) {
550+
logStringBundlingError(moduleName) {
551+
log.error(
552+
"Module " + moduleName + " requires top level scope and can only be embedded as a string " +
553+
"(requires 'eval'), which is not supported with specVersion 4.0 and newer");
554+
}
555+
556+
async checkForStringBundling(moduleName) {
557+
if (!this.allowStringBundling && /\.js$/.test(moduleName)) {
558+
const resource = await this.pool.findResourceWithInfo(moduleName);
559+
if (resource.info?.requiresTopLevelScope) {
560+
this.logStringBundlingError(moduleName);
561+
return null;
562+
}
563+
}
564+
return moduleName;
565+
}
566+
567+
async writeBundleInfos(sections) {
546568
this.outW.ensureNewLine();
547569

548570
let bundleInfoStr = "";
549571
if ( sections.length > 0 ) {
550572
bundleInfoStr = "sap.ui.loader.config({bundlesUI5:{\n";
551-
sections.forEach((section, idx) => {
552-
if ( idx > 0 ) {
573+
let initial = true;
574+
for (let idx = 0; idx < sections.length; idx++) {
575+
const section = sections[idx];
576+
577+
// Remove modules requiring string bundling
578+
let modules = await Promise.all(section.modules.map(this.checkForStringBundling.bind(this)));
579+
modules = modules.filter(($) => $) || [];
580+
581+
if (!initial) {
553582
bundleInfoStr += ",\n";
583+
} else {
584+
initial = false;
554585
}
555586

556587
if (!section.name) {
@@ -561,8 +592,8 @@ class BundleBuilder {
561592
`The info might not work as expected. ` +
562593
`The name must match the bundle filename (incl. extension such as '.js')`);
563594
}
564-
bundleInfoStr += `"${section.name}":[${section.modules.map(makeStringLiteral).join(",")}]`;
565-
});
595+
bundleInfoStr += `"${section.name}":[${modules.map(makeStringLiteral).join(",")}]`;
596+
}
566597
bundleInfoStr += "\n}});\n";
567598

568599
this.writeWithSourceMap(bundleInfoStr);

lib/lbt/bundle/ResolvedBundleDefinition.js

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class ResolvedBundleDefinition {
3838
);
3939
}
4040

41-
createModuleInfo(pool) {
41+
createModuleInfo(pool, allowStringBundling) {
4242
const bundleInfo = new ModuleInfo();
4343
bundleInfo.name = this.name;
4444

@@ -66,7 +66,9 @@ class ResolvedBundleDefinition {
6666
modules.map( (submodule) => {
6767
return pool.getModuleInfo(submodule).then(
6868
(subinfo) => {
69-
if (!bundleInfo.subModules.includes(subinfo.name)) {
69+
if (!bundleInfo.subModules.includes(subinfo.name) &&
70+
(!subinfo.requiresTopLevelScope ||
71+
(subinfo.requiresTopLevelScope && allowStringBundling))) {
7072
bundleInfo.addSubModule(subinfo);
7173
}
7274
}
@@ -78,19 +80,6 @@ class ResolvedBundleDefinition {
7880

7981
return promise.then( () => bundleInfo );
8082
}
81-
82-
/*
83-
public JSModuleDefinition getDefinition() {
84-
return moduleDefinition;
85-
}
86-
87-
public Configuration getConfiguration() {
88-
return moduleDefinition.getConfiguration();
89-
}
90-
91-
92-
}
93-
*/
9483
}
9584

9685
class ResolvedSection {

lib/processors/bundlers/moduleBundler.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,13 @@ const log = getLogger("builder:processors:bundlers:moduleBundler");
135135
* @param {string} [parameters.options.targetUi5CoreVersion] Optional semver compliant sap.ui.core project version, e.g '2.0.0'.
136136
This allows the bundler to make assumptions on available runtime APIs.
137137
Omit if the ultimate UI5 version at runtime is unknown or can't be determined.
138-
* @param {@ui5/project/build/helpers/TaskUtil|object} [parameters.taskUtil] TaskUtil
138+
* @param {boolean} [parameters.options.allowStringBundling=false] Optional flag to allow bundling of modules as a string.
139139
* @returns {Promise<module:@ui5/builder/processors/bundlers/moduleBundler~ModuleBundlerResult[]>}
140140
* Promise resolving with module bundle resources
141141
*/
142142
/* eslint-enable max-len */
143143
export default function({resources, options: {
144-
bundleDefinition, bundleOptions, moduleNameMapping, targetUi5CoreVersion
144+
bundleDefinition, bundleOptions, moduleNameMapping, targetUi5CoreVersion, allowStringBundling = false
145145
}}) {
146146
// Apply defaults without modifying the passed object
147147
bundleOptions = Object.assign({}, {
@@ -158,7 +158,7 @@ export default function({resources, options: {
158158
const pool = new LocatorResourcePool({
159159
ignoreMissingModules: bundleOptions.ignoreMissingModules
160160
});
161-
const builder = new BundleBuilder(pool, targetUi5CoreVersion);
161+
const builder = new BundleBuilder(pool, targetUi5CoreVersion, allowStringBundling);
162162

163163
if (log.isLevelEnabled("verbose")) {
164164
log.verbose(`Generating bundle:`);

lib/tasks/bundlers/generateBundle.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,12 @@ export default async function({
9696
});
9797
}
9898
const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion();
99+
const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0");
99100
return combo.byGlob("/resources/**/*.{js,json,xml,html,properties,library,js.map}").then((resources) => {
100101
const options = {
101-
bundleDefinition: applyDefaultsToBundleDefinition(bundleDefinition, taskUtil), bundleOptions
102+
bundleDefinition: applyDefaultsToBundleDefinition(bundleDefinition, taskUtil),
103+
bundleOptions,
104+
allowStringBundling
102105
};
103106
if (!optimize && taskUtil) {
104107
options.moduleNameMapping = createModuleNameMapping({resources, taskUtil});

lib/tasks/bundlers/generateComponentPreload.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,14 +146,16 @@ export default async function({
146146
});
147147
}
148148
const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion();
149+
const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0");
149150
return Promise.all(bundleDefinitions.filter(Boolean).map((bundleDefinition) => {
150151
log.verbose(`Generating ${bundleDefinition.name}...`);
151152
const options = {
152153
bundleDefinition: applyDefaultsToBundleDefinition(bundleDefinition, taskUtil),
153154
bundleOptions: {
154155
ignoreMissingModules: true,
155156
optimize: true
156-
}
157+
},
158+
allowStringBundling
157159
};
158160
if (coreVersion) {
159161
options.targetUi5CoreVersion = coreVersion;

lib/tasks/bundlers/generateLibraryPreload.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ export default async function({workspace, taskUtil, options: {skipBundles = [],
257257
});
258258
}
259259
const coreVersion = taskUtil?.getProject("sap.ui.core")?.getVersion();
260-
260+
const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0");
261261
const execModuleBundlerIfNeeded = ({options, resources}) => {
262262
if (skipBundles.includes(options.bundleDefinition.name)) {
263263
log.verbose(`Skipping generation of bundle ${options.bundleDefinition.name}`);
@@ -267,6 +267,7 @@ export default async function({workspace, taskUtil, options: {skipBundles = [],
267267
options.targetUi5CoreVersion = coreVersion;
268268
}
269269
options.bundleDefinition = applyDefaultsToBundleDefinition(options.bundleDefinition, taskUtil);
270+
options.allowStringBundling = allowStringBundling;
270271
return moduleBundler({options, resources});
271272
};
272273

lib/tasks/bundlers/generateStandaloneAppBundle.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ export default async function({workspace, dependencies, taskUtil, options}) {
139139
});
140140
}
141141

142+
const allowStringBundling = taskUtil?.getProject().getSpecVersion().lt("4.0");
142143
const bundleOptions = {
143144
bundleDefinition: applyDefaultsToBundleDefinition(
144145
getBundleDefinition({
@@ -149,6 +150,7 @@ export default async function({workspace, dependencies, taskUtil, options}) {
149150
}),
150151
taskUtil
151152
),
153+
allowStringBundling
152154
};
153155

154156
const bundleDbgOptions = {
@@ -164,6 +166,7 @@ export default async function({workspace, dependencies, taskUtil, options}) {
164166
optimize: false,
165167
},
166168
moduleNameMapping: unoptimizedModuleNameMapping,
169+
allowStringBundling
167170
};
168171

169172
if (coreVersion) {
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
sap.ui.define(["sap/ui/core/UIComponent", "sap/m/Button", "application/n/MyModuleRequiringGlobalScope"], (UIComponent, Button) => {
2+
"use strict";
3+
return UIComponent.extend("application.n.Component", {
4+
metadata: {
5+
manifest: "json"
6+
},
7+
createContent() {
8+
return new Button({text: magic.text});
9+
}
10+
});
11+
});

test/expected/build/application.n/dest/Component.js

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

test/expected/build/application.n/dest/Component.js.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)