Skip to content

Commit 7a560b4

Browse files
authored
[FIX] Improve recognition of main module in case of bundles (#341)
This is a port of several changes from the Java bundle tooling, which have been made after the initial migration of the tooling. Additionally, code coverage has been slightly improved by adding further unit and integration tests. Errors that have been revealed by the new tests have been fixed, too. Details about the migrated changes: ---- Change 3312369 (improve recognition of main module in case of bundles): So far, the module analyzer always assumed the first module definition to be the main definition. This failed at least for CVOM bundles but might also fail for others. The analysis has been improved and now accepts a module as main module only in the following cases: - if it is an unnamed module - if it is a named module whose name matches the external name - if it is a named module and there's only one module (no bundle) If multiple module definitions match the above criteria, this is reported as an error. Additionally, AMD special dependencies ('require', 'exports', 'module') are no longer reported in the dependency info. ---- Change 3945349 (JSModuleAnalyzer fails on modules that don't declare a name) Basically, the JavaScript implementation was less sensitive to this issue. But if a resource could have a dependency to another resource named "null", the JavaScript tooling would fail, too. Although this looked like a purely theoretical scenario, adding a protective 'if' was trivial enough to just do it. ---- Change 3562114 (fix dep. analysis of new evo bundles) - recognize and evaluate sap.ui.require.preload calls - write and extract bundle name and raw module names
1 parent b1d727e commit 7a560b4

File tree

47 files changed

+1062
-147
lines changed

Some content is hidden

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

47 files changed

+1062
-147
lines changed

lib/lbt/analyzer/JSModuleAnalyzer.js

Lines changed: 139 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,6 @@ const CALL_AMD_DEFINE = ["define"];
205205
const CALL_AMD_REQUIRE = ["require"];
206206
const CALL_REQUIRE_SYNC = ["require", "sync"];
207207
const CALL_REQUIRE_PREDEFINE = ["require", "predefine"];
208-
const CALL_REQUIRE_PRELOAD = ["require", "preload"];
209208
const CALL_SAP_UI_DEFINE = ["sap", "ui", "define"];
210209
const CALL_SAP_UI_REQUIRE = ["sap", "ui", "require"];
211210
const CALL_SAP_UI_REQUIRE_SYNC = ["sap", "ui", "requireSync"];
@@ -215,11 +214,21 @@ const CALL_JQUERY_SAP_DECLARE = [["jQuery", "$"], "sap", "declare"];
215214
const CALL_JQUERY_SAP_IS_DECLARED = [["jQuery", "$"], "sap", "isDeclared"];
216215
const CALL_JQUERY_SAP_REQUIRE = [["jQuery", "$"], "sap", "require"];
217216
const CALL_JQUERY_SAP_REGISTER_PRELOADED_MODULES = [["jQuery", "$"], "sap", "registerPreloadedModules"];
217+
const SPECIAL_AMD_DEPENDENCIES = ["require", "exports", "module"];
218+
218219

219220
function isCallableExpression(node) {
220221
return node.type == Syntax.FunctionExpression || node.type == Syntax.ArrowFunctionExpression;
221222
}
222223

224+
/*
225+
* Dummy implementation.
226+
* Sole purpose is to easier align with the old (Java) implementation of the bundle tooling.
227+
*/
228+
function getDocumentation(node) {
229+
return undefined;
230+
}
231+
223232
/**
224233
* Analyzes an already parsed JSDocument to collect information about the contained module(s).
225234
*
@@ -231,21 +240,75 @@ function isCallableExpression(node) {
231240
*/
232241
class JSModuleAnalyzer {
233242
analyze(ast, defaultName, info) {
234-
let nModuleDeclarations = 0;
243+
let mainModuleFound = false;
244+
/**
245+
* Number of (sap.ui.)define calls without a module ID.
246+
* Only tracked to be able to complain about multiple module definitions without ID.
247+
*/
248+
let nUnnamedDefines = 0;
249+
/**
250+
* ID of the first named (sap.ui.)define call.
251+
* Remembered together with the corresponding description in case no other main module
252+
* can be found (no unnamed module, no module with the ID that matches the filename).
253+
* Will be used as main module ID if only one module definition exists in the file.
254+
*/
255+
let candidateName = null;
256+
let candidateDescription = null;
235257

236-
// console.log(JSON.stringify(ast, null, " "));
258+
/**
259+
* Total number of module declarations (declare or define).
260+
*/
261+
let nModuleDeclarations = 0;
237262

263+
// first analyze the whole AST...
238264
visit(ast, false);
239265

266+
// ...then all the comments
267+
if ( Array.isArray(ast.comments) ) {
268+
ast.comments.forEach((comment) => {
269+
if ( comment.type === "Line" && comment.value.startsWith("@ui5-bundle") ) {
270+
if ( comment.value.startsWith("@ui5-bundle-raw-include ") ) {
271+
const subModule = comment.value.slice("@ui5-bundle-raw-include ".length);
272+
info.addSubModule(subModule);
273+
log.debug(`bundle include directive ${subModule}`);
274+
} else if ( comment.value.startsWith("@ui5-bundle ") ) {
275+
const bundleName = comment.value.slice("@ui5-bundle ".length);
276+
setMainModuleInfo(bundleName, null);
277+
log.debug(`bundle name directive ${bundleName}`);
278+
} else {
279+
log.warn(`unrecognized bundle directive ${comment.value}`);
280+
}
281+
}
282+
});
283+
}
284+
285+
// ...and finally take conclusions about the file's content
286+
if ( !mainModuleFound ) {
287+
// if there's exactly one module definition in this file but it didn't
288+
// immediately qualify as main module, make it now the main module
289+
if ( candidateName != null && nModuleDeclarations == 1 ) {
290+
info.name = candidateName;
291+
info.description = candidateDescription;
292+
mainModuleFound = true;
293+
} else {
294+
// no main module found, use the default name
295+
info.name = defaultName;
296+
}
297+
}
298+
299+
// depending on the used module APIs, add an implicit dependency to the loader entry module
240300
if ( info.format === ModuleFormat.UI5_LEGACY ) {
241301
info.addImplicitDependency(UI5ClientConstants.MODULE__JQUERY_SAP_GLOBAL);
242302
} else if ( info.format === ModuleFormat.UI5_DEFINE ) {
303+
// Note: the implicit dependency for sap.ui.define modules points to the standard UI5
304+
// loader config module. A more general approach would be to add a dependency to the loader
305+
// only, but then standard configuration would be missed by dependency resolution
306+
// (to be clarified)
243307
info.addImplicitDependency(UI5ClientConstants.MODULE__UI5LOADER_AUTOCONFIG);
244308
}
245-
if ( nModuleDeclarations === 0 && info.name == null ) {
246-
info.name = defaultName;
247-
}
248-
if ( info.dependencies.length === 0 && info.subModules.length === 0 ) {
309+
310+
if ( nModuleDeclarations === 0 && info.dependencies.length === 0 && info.subModules.length === 0 ) {
311+
// when there are no indicators for module APIs, mark the module as 'raw' module
249312
info.rawModule = true;
250313
}
251314

@@ -260,6 +323,16 @@ class JSModuleAnalyzer {
260323
return;
261324

262325
// hoisted functions
326+
function setMainModuleInfo(name, description) {
327+
if ( mainModuleFound ) {
328+
throw new Error("conflicting main modules found (unnamed + named)");
329+
}
330+
mainModuleFound = true;
331+
info.name = name;
332+
if ( description != null ) {
333+
info.description = description;
334+
}
335+
}
263336

264337
function visit(node, conditional) {
265338
// console.log("visiting ", node);
@@ -336,13 +409,14 @@ class JSModuleAnalyzer {
336409
// recognizes a call to jQuery.sap.require
337410
info.setFormat(ModuleFormat.UI5_LEGACY);
338411
onJQuerySapRequire(node, conditional);
339-
} else if ( isMethodCall(node, CALL_JQUERY_SAP_REGISTER_PRELOADED_MODULES)
340-
|| isMethodCall(node, CALL_REQUIRE_PRELOAD)
341-
|| isMethodCall(node, CALL_SAP_UI_REQUIRE_PRELOAD) ) {
412+
} else if ( isMethodCall(node, CALL_JQUERY_SAP_REGISTER_PRELOADED_MODULES) ) {
342413
// recognizes a call to jQuery.sap.registerPreloadedModules
343-
const legacyCall = isMethodCall(node, CALL_JQUERY_SAP_REGISTER_PRELOADED_MODULES);
344-
info.setFormat( legacyCall ? ModuleFormat.UI5_LEGACY : ModuleFormat.UI5_DEFINE);
345-
onRegisterPreloadedModules(node, legacyCall);
414+
info.setFormat(ModuleFormat.UI5_LEGACY);
415+
onRegisterPreloadedModules(node, /* evoSyntax= */ false);
416+
} else if ( isMethodCall(node, CALL_SAP_UI_REQUIRE_PRELOAD) ) {
417+
// recognizes a call to sap.ui.require.preload
418+
info.setFormat(ModuleFormat.UI5_DEFINE);
419+
onRegisterPreloadedModules(node, /* evoSyntax= */ true);
346420
} else if ( isCallableExpression(node.callee) ) {
347421
// recognizes a scope function declaration + argument
348422
visit(node.arguments, conditional);
@@ -393,16 +467,13 @@ class JSModuleAnalyzer {
393467
const args = node.arguments;
394468
if ( args.length > 0 && isString(args[0]) ) {
395469
const name = ModuleName.fromUI5LegacyName( args[0].value );
396-
if ( nModuleDeclarations === 1 ) {
470+
if ( nModuleDeclarations === 1 && !mainModuleFound) {
397471
// if this is the first declaration, then this is the main module declaration
398472
// note that this overrides an already given name
399-
info.name = name;
400-
/* NODE-TODO
401-
info.description = getDocumentation(node);
402-
*/
473+
setMainModuleInfo(name, getDocumentation(node));
403474
} else if ( nModuleDeclarations > 1 && name === info.name ) {
404475
// ignore duplicate declarations (e.g. in behavior file of design time controls)
405-
log.warn(`duplicate declaration of module ${getLocation(args)} in ${name}`);
476+
log.warn(`duplicate declaration of module name at ${getLocation(args)} in ${name}`);
406477
} else {
407478
// otherwise it is just a submodule declaration
408479
info.addSubModule(name);
@@ -417,29 +488,35 @@ class JSModuleAnalyzer {
417488
const nArgs = args.length;
418489
let i = 0;
419490

491+
// get the documentation from a preceding comment
492+
const desc = getDocumentation(defineCall);
493+
420494
// determine the name of the module
421-
let name = defaultName;
495+
let name = null;
422496
if ( i < nArgs && isString(args[i]) ) {
423497
name = ModuleName.fromRequireJSName( args[i++].value );
424-
}
425-
if ( name == null ) {
426-
throw new TypeError("define/sap.ui.define: module name could not be determined," +
427-
`neither from environment nor from first argument: ${args[i] && args[i].type}`);
428-
}
429-
430-
if ( nModuleDeclarations === 1 ) {
431-
// if this is the first declaration, then this is the main module declaration
432-
info.name = name;
433-
434-
// get the documentation from a preceding comment
435-
/* NODE-TODO
436-
info.description = getDocumentation(defineNode);
437-
*/
438-
} else {
439498
if ( name === defaultName ) {
440-
throw new Error("module name could not be determined");
499+
// hardcoded name equals the file name, so this definition qualifies as main module definition
500+
setMainModuleInfo(name, desc);
501+
} else {
502+
info.addSubModule(name);
503+
if ( candidateName == null ) {
504+
// remember the name and description in case no other module qualifies as main module
505+
candidateName = name;
506+
candidateDescription = desc;
507+
}
508+
}
509+
} else {
510+
nUnnamedDefines++;
511+
if ( nUnnamedDefines > 1 ) {
512+
throw new Error("if multiple modules are contained in a file, only one of them may omit the module ID " + name + " " + nUnnamedDefines);
513+
}
514+
if ( defaultName == null ) {
515+
throw new Error("unnamed module found, but no default name given");
441516
}
442-
info.addSubModule(name);
517+
name = defaultName;
518+
// the first unnamed module definition qualifies as main module
519+
setMainModuleInfo(name, desc);
443520
}
444521

445522
// process array of required modules, if given
@@ -502,42 +579,42 @@ class JSModuleAnalyzer {
502579
}
503580
}
504581

505-
function onRegisterPreloadedModules(node, legacyCall) {
582+
function onRegisterPreloadedModules(node, evoSyntax) {
506583
const args = node.arguments;
507584

508585
// trace.debug("**** registerPreloadedModules detected");
509-
if ( args.length > 0 && args[0].type == Syntax.ObjectExpression ) {
510-
let modules = args[0];
511-
let isNewSyntax = true;
512-
513-
if ( legacyCall ) {
514-
const obj = args[0];
515-
isNewSyntax = false;
516-
const version = findOwnProperty(obj, "version");
517-
if ( version && isString(version) && parseFloat(version.value) >= 2.0 ) {
518-
isNewSyntax = true;
519-
}
520-
modules = findOwnProperty(obj, "modules");
521-
}
586+
let modules = null;
587+
let namesUseLegacyNotation = false;
522588

523-
if ( modules && modules.type == Syntax.ObjectExpression ) {
524-
modules.properties.forEach( function(property) {
525-
let moduleName = getPropertyKey(property);
526-
if ( !isNewSyntax ) {
527-
moduleName = ModuleName.fromUI5LegacyName(moduleName);
528-
}
529-
info.addSubModule(moduleName);
530-
});
531-
} else {
532-
log.warn("Cannot evaluate registerPreloadedModules: '%s'", modules && modules.type);
533-
}
589+
if ( evoSyntax ) {
590+
modules = args[0];
591+
} else {
592+
const obj = args[0];
593+
const version = findOwnProperty(obj, "version");
594+
namesUseLegacyNotation = !(version && isString(version) && parseFloat(version.value) >= 2.0);
595+
modules = findOwnProperty(obj, "modules");
596+
}
597+
if ( modules && modules.type == Syntax.ObjectExpression ) {
598+
modules.properties.forEach( function(property) {
599+
let moduleName = getPropertyKey(property);
600+
if ( namesUseLegacyNotation ) {
601+
moduleName = ModuleName.fromUI5LegacyName(moduleName);
602+
}
603+
info.addSubModule(moduleName);
604+
});
605+
} else {
606+
log.warn("Cannot evaluate registerPreloadedModules: '%s'", modules && modules.type);
534607
}
535608
}
536609

537610
function analyzeDependencyArray(array, conditional, name) {
538611
// console.log(array);
539612
array.forEach( (item) => {
540613
if ( isString(item) ) {
614+
// ignore special AMD dependencies (require, exports, module)
615+
if ( SPECIAL_AMD_DEPENDENCIES.indexOf(item.value) >= 0 ) {
616+
return;
617+
}
541618
let requiredModule;
542619
if (name == null) {
543620
requiredModule = ModuleName.fromRequireJSName( item.value );

lib/lbt/bundle/Builder.js

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const UI5BundleFormat = {
4242
outW.write(`jQuery.sap.registerPreloadedModules(`);
4343
outW.writeln(`{`);
4444
if ( section.name ) {
45-
outW.writeln(`"name":"${section.getSectionName()}",`);
45+
outW.writeln(`"name":"${section.name}",`);
4646
}
4747
outW.writeln(`"version":"2.0",`);
4848
outW.writeln(`"modules":{`);
@@ -73,7 +73,7 @@ const EVOBundleFormat = {
7373
afterPreloads(outW, section) {
7474
outW.write(`}`);
7575
if ( section.name ) {
76-
outW.write(`,"${section.getSectionName()}"`);
76+
outW.write(`,"${section.name}"`);
7777
}
7878
outW.writeln(`);`);
7979
},
@@ -144,8 +144,6 @@ class BundleBuilder {
144144
this.jqglobalAvailable = !resolvedModule.containsGlobal;
145145
this.openModule(resolvedModule.name);
146146

147-
this.writeConfiguration(resolvedModule.configuration); // NODE-TODO configuration currently will be undefined
148-
149147
// create all sections in sequence
150148
for ( const section of resolvedModule.sections ) {
151149
log.verbose(" adding section%s of type %s",
@@ -169,6 +167,7 @@ class BundleBuilder {
169167
this.outW = new BundleWriter();
170168
this.missingRawDeclarations = [];
171169

170+
this.outW.writeln("//@ui5-bundle " + module);
172171
if ( this.shouldDecorate ) {
173172
this.outW.writeln(`window["sap-ui-optimized"] = true;`);
174173
if ( this.options.addTryCatchRestartWrapper ) {
@@ -230,31 +229,15 @@ class BundleBuilder {
230229
}
231230
}
232231

233-
writeConfiguration(config) {
234-
if ( !config ) {
235-
return;
236-
}
237-
const outW = this.outW;
238-
outW.ensureNewLine(); // for clarity and to avoid issues with single line comments
239-
outW.writeln(`(function(window){`);
240-
outW.writeln(`\tvar cfg=window['sap-ui-config']=window['sap-ui-config']||{},`);
241-
outW.writeln(`\t\troots=cfg.resourceRoots=cfg.resourceRoots||{};`);
242-
config.propertyName.forEach( (property) => {
243-
outW.writeln(`\tcfg[${makeStringLiteral(property)}]=${config.getPropertyAsJSLiteral(property)};`);
244-
});
245-
Object.keys(config.resourceRoots).forEach( (prefix) => {
246-
outW.writeln(`\troots[${makeStringLiteral(prefix)}]=${makeStringLiteral(config.resourceRoots[prefix])};`);
247-
});
248-
outW.writeln(`}(window));`);
249-
}
250-
251232
// TODO check that there are only JS modules contained
252233
async writeRaw(section) {
253234
// write all modules in sequence
254235
for ( const module of section.modules ) {
255236
const resource = await this.pool.findResourceWithInfo(module);
256237
if ( resource != null ) {
257238
this.outW.startSegment(module);
239+
this.outW.ensureNewLine();
240+
this.outW.writeln("//@ui5-bundle-raw-include " + module);
258241
await this.writeRawModule(module, resource);
259242
const compressedSize = this.outW.endSegment();
260243
log.verbose(" %s (%d,%d)", module, resource.info != null ? resource.info.size : -1, compressedSize);

lib/lbt/bundle/ResolvedBundleDefinition.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,13 @@ class ResolvedSection {
9999
return this.sectionDefinition.mode;
100100
}
101101

102-
/*
103-
public String getSectionName() {
104-
return sectionDefinition.getSectionName();
102+
get name() {
103+
return this.sectionDefinition.name;
105104
}
106105

107-
public boolean isDeclareRawModules() {
108-
return sectionDefinition.isDeclareRawModules();
106+
get declareRawModules() {
107+
return this.sectionDefinition.declareRawModules;
109108
}
110-
111-
*/
112109
}
113110

114111
module.exports = ResolvedBundleDefinition;

0 commit comments

Comments
 (0)