@@ -205,7 +205,6 @@ const CALL_AMD_DEFINE = ["define"];
205205const CALL_AMD_REQUIRE = [ "require" ] ;
206206const CALL_REQUIRE_SYNC = [ "require" , "sync" ] ;
207207const CALL_REQUIRE_PREDEFINE = [ "require" , "predefine" ] ;
208- const CALL_REQUIRE_PRELOAD = [ "require" , "preload" ] ;
209208const CALL_SAP_UI_DEFINE = [ "sap" , "ui" , "define" ] ;
210209const CALL_SAP_UI_REQUIRE = [ "sap" , "ui" , "require" ] ;
211210const CALL_SAP_UI_REQUIRE_SYNC = [ "sap" , "ui" , "requireSync" ] ;
@@ -215,11 +214,21 @@ const CALL_JQUERY_SAP_DECLARE = [["jQuery", "$"], "sap", "declare"];
215214const CALL_JQUERY_SAP_IS_DECLARED = [ [ "jQuery" , "$" ] , "sap" , "isDeclared" ] ;
216215const CALL_JQUERY_SAP_REQUIRE = [ [ "jQuery" , "$" ] , "sap" , "require" ] ;
217216const CALL_JQUERY_SAP_REGISTER_PRELOADED_MODULES = [ [ "jQuery" , "$" ] , "sap" , "registerPreloadedModules" ] ;
217+ const SPECIAL_AMD_DEPENDENCIES = [ "require" , "exports" , "module" ] ;
218+
218219
219220function 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 */
232241class 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 ) ;
0 commit comments