diff --git a/lib/builder/builder.js b/lib/builder/builder.js index 8ea6caa99..d79e39c19 100644 --- a/lib/builder/builder.js +++ b/lib/builder/builder.js @@ -60,6 +60,11 @@ function composeTaskList({dev, selfContained, jsdoc, includedTasks, excludedTask selectedTasks.generateCachebusterInfo = false; selectedTasks.generateApiIndex = false; + // Disable generateResourcesJson due to performance. + // When executed it analyzes each module's AST and therefore + // takes up much time (~10% more) + selectedTasks.generateResourcesJson = false; + if (selfContained) { // No preloads, bundle only selectedTasks.generateComponentPreload = false; diff --git a/lib/lbt/analyzer/JSModuleAnalyzer.js b/lib/lbt/analyzer/JSModuleAnalyzer.js index a25694fe8..67821e3a2 100644 --- a/lib/lbt/analyzer/JSModuleAnalyzer.js +++ b/lib/lbt/analyzer/JSModuleAnalyzer.js @@ -239,6 +239,13 @@ function getDocumentation(node) { * @private */ class JSModuleAnalyzer { + /** + * Analyzes the JS AST + * + * @param {object} ast js ast + * @param {string} defaultName default name + * @param {ModuleInfo} info module info + */ analyze(ast, defaultName, info) { let mainModuleFound = false; /** @@ -260,6 +267,20 @@ class JSModuleAnalyzer { */ let nModuleDeclarations = 0; + /** + * Whether or not this is a UI5 module + * + * When in the non-conditional module execution there is a call to: + * + * this value is true + * + * @type {boolean} + */ + let bIsUi5Module = false; + // first analyze the whole AST... visit(ast, false); @@ -307,7 +328,7 @@ class JSModuleAnalyzer { info.addImplicitDependency(UI5ClientConstants.MODULE__UI5LOADER_AUTOCONFIG); } - if ( nModuleDeclarations === 0 && info.dependencies.length === 0 && info.subModules.length === 0 ) { + if ( !bIsUi5Module ) { // when there are no indicators for module APIs, mark the module as 'raw' module info.rawModule = true; } @@ -320,7 +341,6 @@ class JSModuleAnalyzer { // console.log(info.name, "exposed globals", info.exposedGlobals, "ignoredGlobals", info.ignoredGlobals); } - return; // hoisted functions function setMainModuleInfo(name, description) { @@ -353,6 +373,7 @@ class JSModuleAnalyzer { // recognized a call to jQuery.sap.declare() nModuleDeclarations++; info.setFormat(ModuleFormat.UI5_LEGACY); + bIsUi5Module = true; onDeclare(node); } else if ( !conditional && (isMethodCall(node, CALL_SAP_UI_DEFINE) || isMethodCall(node, CALL_AMD_DEFINE)) ) { @@ -364,6 +385,7 @@ class JSModuleAnalyzer { } else { info.setFormat(ModuleFormat.AMD); } + bIsUi5Module = true; onDefine(node); const args = node.arguments; @@ -380,8 +402,24 @@ class JSModuleAnalyzer { } } else if ( isMethodCall(node, CALL_REQUIRE_PREDEFINE) || isMethodCall(node, CALL_SAP_UI_PREDEFINE) ) { // recognized a call to require.predefine() or sap.ui.predefine() + if (!conditional) { + bIsUi5Module = true; + } info.setFormat(ModuleFormat.UI5_DEFINE); - onSapUiPredefine(node); + onSapUiPredefine(node, conditional); + + const args = node.arguments; + let iArg = 0; + if ( iArg < args.length && isString(args[iArg]) ) { + iArg++; + } + if ( iArg < args.length && args[iArg].type == Syntax.ArrayExpression ) { + iArg++; + } + if ( iArg < args.length && isCallableExpression(args[iArg]) ) { + // unconditionally execute the factory function + visit(args[iArg].body, conditional); + } } else if ( isMethodCall(node, CALL_SAP_UI_REQUIRE) || isMethodCall(node, CALL_AMD_REQUIRE) ) { // recognized a call to require() or sap.ui.require() if ( isMethodCall(node, CALL_SAP_UI_REQUIRE) ) { @@ -404,6 +442,7 @@ class JSModuleAnalyzer { } else if ( isMethodCall(node, CALL_REQUIRE_SYNC) || isMethodCall(node, CALL_SAP_UI_REQUIRE_SYNC) ) { // recognizes a call to sap.ui.requireSync info.setFormat(ModuleFormat.UI5_DEFINE); + onSapUiRequireSync(node, conditional); } else if ( isMethodCall(node, CALL_JQUERY_SAP_REQUIRE) ) { // recognizes a call to jQuery.sap.require @@ -411,10 +450,16 @@ class JSModuleAnalyzer { onJQuerySapRequire(node, conditional); } else if ( isMethodCall(node, CALL_JQUERY_SAP_REGISTER_PRELOADED_MODULES) ) { // recognizes a call to jQuery.sap.registerPreloadedModules + if (!conditional) { + bIsUi5Module = true; + } info.setFormat(ModuleFormat.UI5_LEGACY); onRegisterPreloadedModules(node, /* evoSyntax= */ false); } else if ( isMethodCall(node, CALL_SAP_UI_REQUIRE_PRELOAD) ) { // recognizes a call to sap.ui.require.preload + if (!conditional) { + bIsUi5Module = true; + } info.setFormat(ModuleFormat.UI5_DEFINE); onRegisterPreloadedModules(node, /* evoSyntax= */ true); } else if ( isCallableExpression(node.callee) ) { @@ -560,6 +605,7 @@ class JSModuleAnalyzer { if ( i < nArgs ) { if ( isString(args[i]) ) { + // sap.ui.requireSync does not support relative dependencies const moduleName = ModuleName.fromRequireJSName( args[i].value ); info.addDependency(moduleName, conditional); } else { @@ -569,7 +615,7 @@ class JSModuleAnalyzer { } } - function onSapUiPredefine(predefineCall) { + function onSapUiPredefine(predefineCall, conditional) { const args = predefineCall.arguments; const nArgs = args.length; let i = 0; @@ -578,6 +624,16 @@ class JSModuleAnalyzer { if ( i < nArgs && isString(args[i]) ) { const moduleName = ModuleName.fromRequireJSName( args[i++].value ); info.addSubModule(moduleName); + + // add dependencies + // to correctly identify dependencies e.g. of a library-preload + const elementArg = args[i++]; + if (elementArg && elementArg.type === Syntax.ArrayExpression) { + elementArg.elements.forEach((element) => { + const dependencyName = ModuleName.resolveRelativeRequireJSName(moduleName, element.value); + info.addDependency(dependencyName, conditional); + }); + } } else { log.warn("sap.ui.predefine call is missing a module name (ignored)"); } diff --git a/lib/lbt/bundle/Builder.js b/lib/lbt/bundle/Builder.js index 7b6686d55..6892d3eb2 100644 --- a/lib/lbt/bundle/Builder.js +++ b/lib/lbt/bundle/Builder.js @@ -20,7 +20,7 @@ const {SectionType} = require("./BundleDefinition"); const BundleWriter = require("./BundleWriter"); const log = require("@ui5/logger").getLogger("lbt:bundle:Builder"); -const copyrightCommentsPattern = /copyright|\(c\)(?:[0-9]+|\s+[0-9A-za-z])|released under|license|\u00a9/i; +const copyrightCommentsPattern = /copyright|\(c\)(?:[0-9]+|\s+[0-9A-za-z])|released under|license|\u00a9|^@ui5-bundle-raw-include |^@ui5-bundle /i; const xmlHtmlPrePattern = /<(?:\w+:)?pre>/; const strReplacements = { diff --git a/lib/lbt/resources/LocatorResource.js b/lib/lbt/resources/LocatorResource.js new file mode 100644 index 000000000..0724e6698 --- /dev/null +++ b/lib/lbt/resources/LocatorResource.js @@ -0,0 +1,24 @@ +const Resource = require("./Resource"); + + +function extractName(path) { + return path.slice( "/resources/".length); +} + + +class LocatorResource extends Resource { + constructor(pool, resource) { + super(pool, extractName(resource.getPath()), null, resource.getStatInfo()); + this.resource = resource; + } + + buffer() { + return this.resource.getBuffer(); + } + + getProject() { + return this.resource._project; + } +} + +module.exports = LocatorResource; diff --git a/lib/lbt/resources/LocatorResourcePool.js b/lib/lbt/resources/LocatorResourcePool.js new file mode 100644 index 000000000..c617bac4b --- /dev/null +++ b/lib/lbt/resources/LocatorResourcePool.js @@ -0,0 +1,20 @@ +const ResourcePool = require("./ResourcePool"); +const LocatorResource = require("./LocatorResource"); + + +class LocatorResourcePool extends ResourcePool { + constructor() { + super(); + } + + prepare(resources) { + resources = resources.filter( (res) => !res.getStatInfo().isDirectory() ); + return Promise.all( + resources.map( + (resource) => this.addResource( new LocatorResource(this, resource) ) + ).filter(Boolean) + ); + } +} + +module.exports = LocatorResourcePool; diff --git a/lib/lbt/resources/ModuleInfo.js b/lib/lbt/resources/ModuleInfo.js index bcdb3011e..1e0a2b8b2 100644 --- a/lib/lbt/resources/ModuleInfo.js +++ b/lib/lbt/resources/ModuleInfo.js @@ -58,6 +58,12 @@ class ModuleInfo { /** * 'raw' modules are modules that don't use UI5's module system (require/declare) * TODO align with module format (ui5, amd, es6, raw) + * + * A raw module is a module which does not have in its non-conditional execution: + * */ this.rawModule = false; diff --git a/lib/lbt/resources/ResourceCollector.js b/lib/lbt/resources/ResourceCollector.js new file mode 100644 index 000000000..f9cf97f5b --- /dev/null +++ b/lib/lbt/resources/ResourceCollector.js @@ -0,0 +1,312 @@ +const ResourceInfoList = require("./ResourceInfoList"); +const ResourceFilterList = require("./ResourceFilterList"); +const ResourceInfo = require("./ResourceInfo"); + + +const LOCALE = /^((?:[^/]+\/)*[^/]+?)_([A-Z]{2}(?:_[A-Z]{2}(?:_[A-Z0-9_]+)?)?)(\.properties|\.hdbtextbundle)$/i; +const THEME = /^((?:[^/]+\/)*)themes\/([^/]+)\//; + +const log = require("@ui5/logger").getLogger("lbt:resources:ResourceCollector"); + +/** + * Collects all resources in a set of resource folders or from an archive + * and writes a 'resources.json' file for each component or library + * that can be found in the resources. + * + * @since 1.29.0 + */ +class ResourceCollector { + /** + * Collects a set of ResourceInfo objects and groups them by components, libraries and themes. + * + * @param {ResourcePool} pool + * @param {ResourceFilterList} [filter] used to filter the resources based on their name + */ + constructor(pool, filter) { + this._pool = pool; + /** + * Global filters that should be taken into account when collecting resources. + * + * @type {ResourceFilterList} + * @private + */ + this._filter = filter != null ? filter : new ResourceFilterList(); + /** + * name to resource info + * + * @type {Map} + */ + this._resources = new Map(); + + /** + * prefix to ResourceInfoList + * + * @type {Map} + * @private + */ + this._components = new Map(); + + /** + * prefix to ResourceInfoList + * + * @type {Map} + * @private + */ + this._themePackages = new Map(); + } + + /** + * Comma separated list of components to which orphaned resources should be added. + * + * A component and a separated list of resource patterns of orphans that should be added + * to the preceding component. + * If no such list is given, any orphaned resource will be added to the component. + * The evaluation logic for the filter list is the same as for the filters + * parameters: excludes can be denoted with a leading '-' or '!' and order is significant. + * Later filters can override the result of earlier ones. + * + * If no component is given, orphans will only be reported but not added to any component (default). + * + * @param {object} list component to list of components + */ + setExternalResources(list) { + this._externalResources = list; + } + + /** + * Processes a resource + * + * @param {module:@ui5/fs.Resource} resource + */ + async visitResource(resource) { + const virPath = resource.getPath(); + if ( !virPath.startsWith("/resources/") ) { + log.warn(`non-runtime resource ${virPath} ignored`); + return; + } + const name = virPath.slice("/resources/".length); + if ( this._filter.matches(name) ) { + const resourceInfo = new ResourceInfo(name); + resourceInfo.size = await resource.getSize(); + this._resources.set(name, resourceInfo); + + const p = name.lastIndexOf("/"); + const prefix = name.substring(0, p + 1); + const basename = name.substring(p + 1); + if ( basename.match("[^/]*\\.library|Component\\.js|manifest\\.json") && !this._components.has(prefix) ) { + this._components.set(prefix, new ResourceInfoList(prefix)); + } + // a .theme file within a theme folder indicates a library/theme package + // Note: ignores .theme files in library folders + + // .theming files are not always present therefore this check is relevant for the library.source.less + if ( name.match("(?:[^/]+/)*themes/[^/]+/(?:\\.theming|library\\.source\\.less)") && !this._themePackages.has(prefix) ) { + // log.info("found new theme package %s", prefix); + this._themePackages.set(prefix, new ResourceInfoList(prefix)); + } + } + } + + async enrichWithDependencyInfo(resourceInfo) { + return this._pool.getModuleInfo(resourceInfo.name).then((moduleInfo) => { + if ( moduleInfo.name ) { + resourceInfo.module = moduleInfo.name; + } + if ( moduleInfo.dynamicDependencies ) { + resourceInfo.dynRequired = true; + } + if ( moduleInfo.dependencies.length > 0 ) { + resourceInfo.required = resourceInfo.required || new Set(); + resourceInfo.condRequired = resourceInfo.condRequired || new Set(); + moduleInfo.dependencies.forEach((dep) => { + if ( moduleInfo.isConditionalDependency(dep) ) { + resourceInfo.condRequired.add(dep); + } else if ( !moduleInfo.isImplicitDependency(dep) ) { + resourceInfo.required.add(dep); + } + }); + } + if ( moduleInfo.subModules.length > 0 ) { + resourceInfo.included = resourceInfo.included || new Set(); + moduleInfo.subModules.forEach((mod) => { + resourceInfo.included.add(mod); + }); + } + + if (moduleInfo.requiresTopLevelScope) { + resourceInfo.requiresTopLevelScope = true; + } + if (moduleInfo.exposedGlobals != null && moduleInfo.exposedGlobals.length) { + resourceInfo.exposedGlobalNames = resourceInfo.exposedGlobalNames || new Set(); + moduleInfo.exposedGlobals.forEach((exposedGlobalName) => { + resourceInfo.exposedGlobalNames.add(exposedGlobalName); + }); + } + + if (moduleInfo.rawModule) { + resourceInfo.format = "raw"; + } + }); + } + + async determineResourceDetails({pool, debugResources, mergedResources, designtimeResources, supportResources}) { + const baseNames = new Set(); + const debugFilter = new ResourceFilterList(debugResources); + const mergeFilter = new ResourceFilterList(mergedResources); + const designtimeFilter = new ResourceFilterList(designtimeResources); + const supportFilter = new ResourceFilterList(supportResources); + + const promises = []; + + for (const [name, info] of this._resources.entries()) { + // log.verbose(` checking ${name}`); + let m; + if ( m = LOCALE.exec(name) ) { + const baseName = m[1] + m[3]; + log.verbose(` found potential i18n resource '${name}', base name is '${baseName}', locale is ${m[2]}`); + info.i18nName = baseName; // e.g. "i18n.properties" + info.i18nLocale = m[2]; // e.g. "de" + baseNames.add(baseName); + } + + if ( m = THEME.exec(name) ) { + // log.verbose("found theme candidate %s with prefix %s", name, m[1]); + if ( this._themePackages.has(m[0]) ) { + const theme = m[2]; + info.theme = theme; + log.verbose(` found potential theme resource '${name}', theme ${theme}`); + } + } + + if ( /(?:\.js|\.view\.xml|\.control\.xml|\.fragment\.xml)$/.test(name) ) { + promises.push( + this.enrichWithDependencyInfo(info) + ); + } + + // set the module name for .properties and .json + if ( /(?:\.properties|\.json)$/.test(name) ) { + promises.push(new Promise((resolve) => { + return this._pool.getModuleInfo(info.name).then((moduleInfo) => { + if (moduleInfo.name) { + info.module = moduleInfo.name; + } + resolve(); + }); + })); + } + + if ( debugFilter.matches(name) ) { + info.isDebug = true; + log.verbose(` found potential debug resource '${name}'`); + } + + if ( mergeFilter.matches(name) ) { + info.merged = true; + log.verbose(` found potential merged resource '${name}'`); + } + + if ( designtimeFilter.matches(name) ) { + info.designtime = true; + log.verbose(` found potential designtime resource '${name}'`); + } + + if ( supportFilter.matches(name) ) { + info.support = true; + log.verbose(` found potential support resource '${name}'`); + } + } + + for (const baseName of baseNames) { + if ( this._resources.has(baseName) ) { + const info = this._resources.get(baseName); + info.i18nName = baseName; + info.i18nLocale = ""; + } + } + + return Promise.all(promises); + } + + createOrphanFilters() { + log.verbose( + ` configured external resources filters (resources outside the namespace): ${this._externalResources == null ? "(none)" : this._externalResources}`); + + const filtersByComponent = new Map(); + + if ( this._externalResources != null ) { + for ( let [component, filters] of Object.entries(this._externalResources) ) { + const packageFilters = new ResourceFilterList(filters); + if ( component === "/" || component === "" ) { + component = ""; + } else if ( !component.endsWith("/") ) { + component += "/"; + } + log.verbose(` resulting filter list for '${component}': '${packageFilters}'`); + filtersByComponent.set(component, packageFilters); + } + } + return filtersByComponent; + } + + groupResourcesByComponents(options) { + const orphanFilters = this.createOrphanFilters(); + const debugBundlesFilter = new ResourceFilterList(options.debugBundles); + for (const resource of this._resources.values()) { + let contained = false; + for (const [prefix, list] of this._components.entries()) { + const isDebugBundle = debugBundlesFilter.matches(resource.name); + if ( resource.name.startsWith(prefix) ) { + list.add(resource, !isDebugBundle); + contained = true; + } else if ( orphanFilters.has(prefix) ) { + // log.verbose(` checking '${resource.name}' against orphan filter '${orphanFilters.get(prefix)}' (${prefix})`); + if ( orphanFilters.get(prefix).matches(resource.name) ) { + list.add(resource, !isDebugBundle); + contained = true; + } + } + } + + if ( resource.theme != null ) { + // assign theme resources additionally to theme packages + for (const [prefix, list] of this._themePackages.entries()) { + if ( resource.name.startsWith(prefix) ) { + list.add(resource); + contained = true; + } + } + } + + if ( contained ) { + this._resources.delete(resource.name); + } + } + } + + /** + * + * @returns {Set} resource names + */ + get resources() { + return new Set(this._resources.keys()); + } + + /** + * Components + * + * @returns {Map} + */ + get components() { + return this._components; + } + + /** + * @returns {Map} + */ + get themePackages() { + return this._themePackages; + } +} + +module.exports = ResourceCollector; diff --git a/lib/lbt/resources/ResourceFilterList.js b/lib/lbt/resources/ResourceFilterList.js index 67e23f53e..e29301177 100644 --- a/lib/lbt/resources/ResourceFilterList.js +++ b/lib/lbt/resources/ResourceFilterList.js @@ -127,6 +127,32 @@ class ResourceFilterList { } } +/** + * Each filter entry can be a comma separated list of simple filters. Each simple filter + * can be a pattern in resource name pattern syntax: A double asterisk '&0x2a;&0x2a;/' denotes an arbitrary + * number of resource name segments (folders) incl. a trailing slash, whereas a simple asterisk '*' + * denotes an arbitrary number of resource name characters, but not the segment separator '/'. + * A dot is interpreted as a dot, all other special regular expression characters keep their + * special meaning. This is a mixture of ANT-style path patterns and regular expressions. + * + * Excludes can be denoted by a leading '-' or '!', includes optionally by a leading '+'. + * Order of filters is significant, a later exclusion overrides an earlier inclusion + * and vice versa. + * + * Example: + *
+ *	 !sap/ui/core/
+ *	 +sap/ui/core/utils/
+ * 
+ * excludes everything from sap/ui/core, but includes everything from the subpackage sap/ui/core/utils/. + * + * Note that the filter operates on the full name of each resource. If a resource name + * prefix is configured for a resource set, the filter will be applied + * to the combination of prefix and local file path and not only to the local file path. + * + * @param {string} filterStr comma separated list of simple filters + * @returns {ResourceFilterList} + */ ResourceFilterList.fromString = function(filterStr) { const result = new ResourceFilterList(); if ( filterStr != null ) { diff --git a/lib/lbt/resources/ResourceInfo.js b/lib/lbt/resources/ResourceInfo.js new file mode 100644 index 000000000..a6a378473 --- /dev/null +++ b/lib/lbt/resources/ResourceInfo.js @@ -0,0 +1,180 @@ +/** + * Information about a single resource as stored in the resources.json file. + * + * @author Frank Weigel + * @since 1.33.0 + */ +class ResourceInfo { + /** + * @param {string} name name of the resource + */ + constructor(name) { + this.name = name; + this.i18nName = null; + this.i18nLocale = null; + this.isDebug = false; + this.theme = null; + this.merged = false; + this.designtime = false; + this.support = false; + this._module = null; + this.required = null; + this.condRequired = null; + this.included = null; + this.dynRequired = false; + this.requiresTopLevelScope = false; + this.exposedGlobalNames = null; + this._format = null; + this._size = -1; + } + + + get module() { + return this._module; + } + + set module(value) { + this._module = value; + } + + get format() { + return this._format; + } + + set format(value) { + this._format = value; + } + + get size() { + return this._size; + } + + set size(value) { + this._size = value; + } + + /** + * Copies the properties of the given ResourceInfo into this + * + * @param {string} prefix + * @param {ResourceInfo} orig + */ + copyFrom(prefix, orig) { + this.i18nName = orig.i18nName; + this.i18nLocale = orig.i18nLocale; + this.isDebug = orig.isDebug; + this.theme = orig.theme; + this.merged = orig.merged; + this.designtime = orig.designtime; + this.support = orig.support; + if ( this.module == null ) { + this.module = orig.module; + } + if ( orig.required != null ) { + if ( this.required == null ) { + this.required = new Set(); + } + orig.required.forEach(this.required.add, this.required); + } + if ( orig.condRequired != null ) { + if ( this.condRequired == null ) { + this.condRequired = new Set(); + } + orig.condRequired.forEach(this.condRequired.add, this.condRequired); + } + if ( orig.dynRequired ) { + this.dynRequired = orig.dynRequired; + } + if ( orig.included != null ) { + if ( this.included == null ) { + this.included = new Set(); + } + orig.included.forEach(this.included.add, this.included); + } + if ( this.included != null && this.included.size > 0 ) { + this.merged = true; + } + if (orig.size >= 0) { + this.size = orig.size; + } + if ( orig.requiresTopLevelScope ) { + this.requiresTopLevelScope = orig.requiresTopLevelScope; + } + if ( orig.exposedGlobalNames != null ) { + if ( this.exposedGlobalNames == null ) { + this.exposedGlobalNames = new Set(); + } + orig.exposedGlobalNames.forEach(this.exposedGlobalNames.add, this.exposedGlobalNames); + } + if ( orig.format != null ) { + this.format = orig.format; + } + } + + /** + * called from JSON.stringify() + * + * @returns {{name: *}} + */ + toJSON() { + const result = { + name: this.name + }; + if ( this._module != null ) { + result.module = this.module; + } + if ( this.size >= 0 ) { + result.size = this.size; + } + if ( this.requiresTopLevelScope ) { + result.requiresTopLevelScope = this.requiresTopLevelScope; + } + if ( this.exposedGlobalNames != null && this.exposedGlobalNames.size > 0 ) { + result.exposedGlobalNames = [...this.exposedGlobalNames]; + } + if ( this.format ) { + result.format = this.format; + } + + // + + if ( this.isDebug ) { + result.isDebug = this.isDebug; + } + if ( this.merged ) { + result.merged = this.merged; + } + if ( this.designtime ) { + result.designtime = this.designtime; + } + if ( this.support ) { + result.support = this.support; + } + if ( this.i18nLocale != null ) { + result.locale = this.i18nLocale; + result.raw = this.i18nName; + } + if ( this.theme != null ) { + result.theme = this.theme; + } + + // + + if ( this.required != null && this.required.size > 0 ) { + result.required = [...this.required].sort(); + } + if ( this.condRequired != null && this.condRequired.size > 0 ) { + result.condRequired = [...this.condRequired].sort(); + } + if ( this.dynRequired ) { + result.dynRequired = this.dynRequired; + } + if ( this.included != null && this.included.size > 0 ) { + result.included = [...this.included]; + } + + return result; + } +} + +module.exports = ResourceInfo; diff --git a/lib/lbt/resources/ResourceInfoList.js b/lib/lbt/resources/ResourceInfoList.js new file mode 100644 index 000000000..af5b655ea --- /dev/null +++ b/lib/lbt/resources/ResourceInfoList.js @@ -0,0 +1,153 @@ +const ResourceInfo = require("./ResourceInfo"); + +const DEBUG_RESOURCES_PATTERN = /-dbg((?:\.view|\.fragment|\.controller|\.designtime|\.support)?\.js|\.css)$/; +const RESOURCES_PATTERN = /((?:\.view|\.fragment|\.controller|\.designtime|\.support)?\.js|\.css)$/; + +/** + * A list of ResourceInfo objects, suitable for (but not dependent on) JSON serialization. + * + * @author Frank Weigel + * @since 1.33.0 + */ +class ResourceInfoList { + /** + * Holds ResourceInfos + * + * @param {string} prefix + */ + constructor(prefix) { + /** + * List of resources information objects + * + * @type {ResourceInfo[]} + */ + this.resources = []; + + // --- transient state --- + /** + * @type {string} name of the resource + */ + this.name = prefix; + /** + * + * @type {Map} + */ + this.resourcesByName = new Map(); + } + + /** + * Add ResourceInfo to list + * + * @param {ResourceInfo} info + * @param {boolean} shareDebugInformation + */ + add(info, shareDebugInformation=true) { + const relativeName = ResourceInfoList.makePathRelativeTo(this.name, info.name); + + // search for a resource with the same name + let myInfo = this.resourcesByName.get(relativeName); + + if ( myInfo == null && shareDebugInformation) { + // when not found, check if the given resource is a debug resource and + // share the information with the non-dbg version + const nonDbgName = ResourceInfoList.getNonDebugName(relativeName); + const dbgName = ResourceInfoList.getDebugName(relativeName); + if ( nonDbgName != null && this.resourcesByName.has(nonDbgName) ) { + // copy from source + myInfo = new ResourceInfo(relativeName); + const source = this.resourcesByName.get(nonDbgName); + myInfo.copyFrom(this.name, source); + this.resources.push(myInfo); + this.resourcesByName.set(relativeName, myInfo); + } else if (dbgName != null && this.resourcesByName.has(dbgName)) { + // copy from debug + myInfo = new ResourceInfo(relativeName); + const source = this.resourcesByName.get(dbgName); + myInfo.copyFrom(this.name, source); + myInfo.module = ResourceInfoList.getNonDebugName(source.module); + this.resources.push(myInfo); + this.resourcesByName.set(relativeName, myInfo); + } + } + + // this is the assumption, that the debug one is the same as the non-dbg one + if ( myInfo == null ) { + myInfo = new ResourceInfo(relativeName); + myInfo.size = info.size; + myInfo.module = ResourceInfoList.getNonDebugName(info.name); + this.resources.push(myInfo); + this.resourcesByName.set(relativeName, myInfo); + } + myInfo.copyFrom(this.name, info); + if (info.i18nName) { + myInfo.i18nName = ResourceInfoList.makePathRelativeTo(this.name, info.i18nName); + } + } + + /** + * Serializes its content to JSON format + * + * @returns {{resources: ResourceInfo[], _version: string}} + */ + toJSON() { + this.resources.sort((a, b) => { + if ( a.name === b.name ) { + return 0; + } + return a.name < b.name ? -1 : 1; + }); + return { + /** + * Version of the resources.json file format, must be 1.1.0 or higher to store dependencies + */ + _version: "1.1.0", + resources: this.resources + }; + } + + /** + * Retrieves the relative path + * + * @param {string} prefix + * @param {string} name + * @returns {string} + */ + static makePathRelativeTo(prefix, name) { + let back = ""; + while ( !name.startsWith(prefix) ) { + const p = prefix.lastIndexOf("/", prefix.length - 2); + back = back + "../"; + if ( p >= 0 ) { + prefix = prefix.slice(0, p + 1); + } else { + prefix = ""; + break; + } + } + return back + name.slice(prefix.length); + } + + /** + * If the given module is a -dbg file, calculate and return the non-dbg name. + * + * @param {string} path + * @returns {string|null} Non-debug name of the module + */ + static getNonDebugName(path) { + if ( DEBUG_RESOURCES_PATTERN.test(path) ) { + return path.replace( DEBUG_RESOURCES_PATTERN, "$1"); + } + return null; + } + + static getDebugName(path) { + if ( RESOURCES_PATTERN.test(path) ) { + if (!path.replace(RESOURCES_PATTERN, "").endsWith("-dbg")) { + return path.replace( RESOURCES_PATTERN, "-dbg$1"); + } + } + return null; + } +} + +module.exports = ResourceInfoList; diff --git a/lib/lbt/resources/ResourcePool.js b/lib/lbt/resources/ResourcePool.js index 0e0e478b2..ed7e45239 100644 --- a/lib/lbt/resources/ResourcePool.js +++ b/lib/lbt/resources/ResourcePool.js @@ -161,6 +161,12 @@ class ResourcePool { } } + /** + * Retrieves the module info + * + * @param {string} name module name + * @returns {Promise} + */ async getModuleInfo(name) { let info = this._dependencyInfos.get(name); if ( info == null ) { diff --git a/lib/processors/bundlers/moduleBundler.js b/lib/processors/bundlers/moduleBundler.js index 673e2e1f8..62e7f24c0 100644 --- a/lib/processors/bundlers/moduleBundler.js +++ b/lib/processors/bundlers/moduleBundler.js @@ -1,45 +1,7 @@ const BundleBuilder = require("../../lbt/bundle/Builder"); -const Resource = require("../../lbt/resources/Resource"); -const ResourcePool = require("../../lbt/resources/ResourcePool"); +const LocatorResourcePool = require("../../lbt/resources/LocatorResourcePool"); const EvoResource = require("@ui5/fs").Resource; -function extractName(path) { - return path.slice( "/resources/".length); -} - -class LocatorResource extends Resource { - constructor(pool, resource) { - super(pool, extractName(resource.getPath()), null, resource.getStatInfo()); - this.resource = resource; - } - - buffer() { - return this.resource.getBuffer(); - } - - getProject() { - return this.resource._project; - } -} - -class LocatorResourcePool extends ResourcePool { - constructor({ignoreMissingModules}) { - super({ignoreMissingModules}); - } - - prepare(resources) { - resources = resources.filter( (res) => !res.getStatInfo().isDirectory() ); - // console.log(resources.map($ => $.getPath())); - return Promise.all( - resources.map( - (resource) => this.addResource( new LocatorResource(this, resource) ) - ).filter( (followUp) => followUp ) - ); - // .then( () => { - // console.log(" found %d resources", this.size); - // }); - } -} /** * A ModuleBundleDefinitionSection specifies the embedding mode ('provided', 'raw', 'preload' or 'require') diff --git a/lib/processors/resourceListCreator.js b/lib/processors/resourceListCreator.js new file mode 100644 index 000000000..20b5034a5 --- /dev/null +++ b/lib/processors/resourceListCreator.js @@ -0,0 +1,200 @@ +"use strict"; + +const log = require("@ui5/logger").getLogger("builder:processors:resourceListCreator"); +const ResourceCollector = require("../lbt/resources/ResourceCollector"); +const LocatorResourcePool = require("../lbt/resources/LocatorResourcePool"); +const ResourceInfo = require("../lbt/resources/ResourceInfo"); + +const EvoResource = require("@ui5/fs").Resource; + +/** + * List of resource patterns that describe all debug resources. + * + * @since 1.29.1 + */ +const DEFAULT_DEBUG_RESOURCES_FILTER = [ + "**/*-dbg.js", + "**/*-dbg.controller.js", + "**/*-dbg.designtime.js", + "**/*-dbg.support.js", + "**/*-dbg.view.js", + "**/*-dbg.fragment.js", + "**/*-dbg.css", + "**/*.js.map" +]; + +/** + * List of resource patterns that describe bundled resources. + * + * @since 1.29.1 + */ +const DEFAULT_BUNDLE_RESOURCES_FILTER = [ + "**/Component-preload.js", + "**/library-preload.js", + "**/library-preload-dbg.js", + "**/library-preload.json", + "**/library-h2-preload.js", + "**/designtime/library-preload.designtime.js", + "**/library-preload.support.js", + "**/library-all.js", + "**/library-all-dbg.js" +]; + +/** + * List of resource patterns that describe all designtime resources. + * + * @since 1.31.0 + */ +const DEFAULT_DESIGNTIME_RESOURCES_FILTER = [ + "**/designtime/*", + "**/*.designtime.js", + "**/*.control", + "**/*.interface", + "**/*.type", + "**/themes/*/*.less", + "**/library.templates.xml", + "**/library.dependencies.xml", + "**/library.dependencies.json" +]; + +/** + * List of resource patterns that describe all support (assistant) resources. + * + * @since 1.53.0 + */ +const DEFAULT_SUPPORT_RESOURCES_FILTER = [ + "**/*.support.js" +]; + +/** + * Hard coded debug bundle, to trigger separate analysis for this filename + * because sap-ui-core.js and sap-ui-core-dbg.js have different includes + * + * @type {string[]} + */ +const DEBUG_BUNDLES = [ + "sap-ui-core-dbg.js" +]; + +/** + * Creates and adds resources.json entry (itself) to the list. + * + * Retrieves the string content of the overall result and returns it. + * + * @param {ResourceInfoList} list resources list + * @param {string} prefix + * @returns {string} new content with resources.json entry + */ +function makeResourcesJSON(list, prefix) { + // having the file size entry part of the file is a bit like the chicken egg scenario + // you can't change the value of the file size without changing the file size + // so this part here tries to cope with that. + + // try to add resources.json entry with previous size of the list string. + // get the content to be added (resources.json entry) + // modify the size of the entry from the calculated one + + let contentString = JSON.stringify(list, null, "\t"); + const resourcesJson = new ResourceInfo(prefix + "resources.json"); + // initial size + resourcesJson.size = Buffer.from(contentString).byteLength; + list.add(resourcesJson); + + contentString = JSON.stringify(list, null, "\t"); + + let newLength = Buffer.from(contentString).byteLength; + + // Adjust size until it is correct + // This entry's size depends on the file size which depends on this entry's size,... + // Updating the number of the size in the content might influence the size of the file itself + // This is deterministic because e.g. in the content -> "size": 1000 has the same amount of bytes as "size": 9999 + // the difference might only come for: + // * adding the initial entry of resources.json + // * changes when the number of digits of the number changes, e.g. 100 -> 1000 + while (resourcesJson.size !== newLength) { + resourcesJson.size = newLength; + list.add(resourcesJson); + contentString = JSON.stringify(list, null, "\t"); + newLength = Buffer.from(contentString).byteLength; + } + return contentString; +} + +/** + * Whether the detection of orphans should result in a build failure. + * + * @since 1.29.1 + * @param {object} configuration + * @param {module:@ui5/fs.Resource[]} configuration.resources + * @param {object} [options] configuration options + * @returns {module:@ui5/fs.Resource[]} list of resources.json files + */ +module.exports = async function({resources}, options) { + options = Object.assign({ + failOnOrphans: true, + externalResources: undefined, + debugResources: DEFAULT_DEBUG_RESOURCES_FILTER, + mergedResources: DEFAULT_BUNDLE_RESOURCES_FILTER, + designtimeResources: DEFAULT_DESIGNTIME_RESOURCES_FILTER, + supportResources: DEFAULT_SUPPORT_RESOURCES_FILTER, + debugBundles: DEBUG_BUNDLES + }, options); + + const pool = new LocatorResourcePool(); + await pool.prepare( resources ); + + const collector = new ResourceCollector(pool); + const visitPromises = resources.map((resource) => collector.visitResource(resource)); + + await Promise.all(visitPromises); + log.verbose(` found ${collector.resources.size} resources`); + + // determine additional information for the found resources + if ( options && options.externalResources ) { + collector.setExternalResources(options.externalResources); + } + + await collector.determineResourceDetails({ + pool, + debugResources: options.debugResources, + mergedResources: options.mergedResources, + designtimeResources: options.designtimeResources, + supportResources: options.supportResources + }); + + // group resources by components and create ResourceInfoLists + collector.groupResourcesByComponents({ + debugBundles: options.debugBundles + }); + + const resourceLists = []; + + // write out resources.json files + for (const [prefix, list] of collector.components.entries()) { + log.verbose(` writing '${prefix}resources.json'`); + + const contentString = makeResourcesJSON(list, prefix); + + resourceLists.push(new EvoResource({ + path: `/resources/${prefix}resources.json`, + string: contentString + })); + } + for (const [prefix, list] of collector.themePackages.entries()) { + log.verbose(` writing '${prefix}resources.json'`); + + const contentString = makeResourcesJSON(list, prefix); + + resourceLists.push(new EvoResource({ + path: `/resources/${prefix}resources.json`, + string: contentString + })); + } + const unassigned = collector.resources; + if ( unassigned.size > 0 && options.failOnOrphans ) { + log.error(`resources.json generation failed because of unassigned resources: ${[...unassigned].join(", ")}`); + throw new Error(`resources.json generation failed with error: There are ${unassigned.size} resources which could not be assigned to components.`); + } + + return resourceLists; +}; diff --git a/lib/processors/uglifier.js b/lib/processors/uglifier.js index a20707397..f5c4c9591 100644 --- a/lib/processors/uglifier.js +++ b/lib/processors/uglifier.js @@ -1,5 +1,16 @@ const terser = require("terser"); -const copyrightCommentsPattern = /copyright|\(c\)(?:[0-9]+|\s+[0-9A-za-z])|released under|license|\u00a9/i; +/** + * Preserve comments which contain: + *
    + *
  • copyright notice
  • + *
  • license terms
  • + *
  • "@ui5-bundle"
  • + *
  • "@ui5-bundle-raw-include"
  • + *
+ * + * @type {RegExp} + */ +const copyrightCommentsAndBundleCommentPattern = /copyright|\(c\)(?:[0-9]+|\s+[0-9A-za-z])|released under|license|\u00a9|^@ui5-bundle-raw-include |^@ui5-bundle /i; /** * Minifies the supplied resources. @@ -18,7 +29,7 @@ module.exports = function({resources}) { }, { warnings: false, output: { - comments: copyrightCommentsPattern, + comments: copyrightCommentsAndBundleCommentPattern, wrap_func_args: false }, compress: false diff --git a/lib/tasks/generateResourcesJson.js b/lib/tasks/generateResourcesJson.js new file mode 100644 index 000000000..399aa524d --- /dev/null +++ b/lib/tasks/generateResourcesJson.js @@ -0,0 +1,115 @@ +"use strict"; + +const resourceListCreator = require("../processors/resourceListCreator"); + +const DEFAULT_EXCLUDES = [ + /* + * exclude mac metadata files + */ + "!**/.DS_Store", + /* + * sap-ui-version.json is not part of the resources + */ + "!/resources/sap-ui-version.json" +]; + +function getCreatorOptions(projectName) { + const creatorOptions = {}; + if ( projectName === "sap.ui.core" ) { + Object.assign(creatorOptions, { + externalResources: { + "sap/ui/core": [ + "*", + "sap/base/", + "sap/ui/" + ] + }, + mergedResourcesFilters: [ + "jquery-sap*.js", + "sap-ui-core*.js", + "**/Component-preload.js", + "**/library-preload.js", + "**/library-preload-dbg.js", + "**/library-preload.json", + "**/library-all.js", + "**/library-all-dbg.js", + "**/designtime/library-preload.designtime.js", + "**/library-preload.support.js" + ] + }); + } + return creatorOptions; +} + +/** + * Task for creating a resources.json file, describing all productive build resources. + * + *

+ * The detailed structure can be found in the documentation: + * {@link https://openui5.hana.ondemand.com/#topic/adcbcf8b50924556ab3f321fcd9353ea} + *

+ * + *

+ * Not supported in combination with task {@link module:@ui5/builder.tasks.generateStandaloneAppBundle}. + * Therefore it is also not supported in combination with self-contained build. + *

+ * + * @example sample resources.json + * { + * "_version": "1.1.0", + * "resources": [ + * { + * "name": "Component-preload.js", + * "module": "application/mine/Component-preload.js", + * "size": 3746, + * "merged": true, + * "included": [ + * "application/mine/Component.js", + * "application/mine/changes/coding/MyExtension.js", + * "application/mine/changes/flexibility-bundle.json", + * "application/mine/changes/fragments/MyFragment.fragment.xml", + * "application/mine/manifest.json" + * ] + * }, + * { + * "name": "resources.json", + * "size": 1870 + * }, + * { + * "name": "rules/Button-dbg.support.js", + * "module": "application/mine/rules/Button.support.js", + * "size": 211, + * "format": "raw", + * "isDebug": true, + * "required": [ + * "application/mine/library.js", + * "sap/ui/core/Control.js" + * ], + * "condRequired": [ + * "application/mine/changeHandler/SplitButton.js", + * "sap/ui/core/format/DateFormat.js" + * ], + * "dynRequired": true, + * "support": true + * } + * ] + * } + * + * @public + * @alias module:@ui5/builder.tasks.generateResourcesJson + * @param {object} parameters Parameters + * @param {module:@ui5/fs.DuplexCollection} parameters.workspace DuplexCollection to read and write files + * @param {object} parameters.options Options + * @param {string} parameters.options.projectName Project name + * @returns {Promise} Promise resolving with undefined once data has been written + */ +module.exports = async function({workspace, options: {projectName}}) { + const resources = await workspace.byGlob(["/resources/**/*.*"].concat(DEFAULT_EXCLUDES)); + + const resourceLists = await resourceListCreator({ + resources + }, getCreatorOptions(projectName)); + await Promise.all( + resourceLists.map((resourceList) => workspace.write(resourceList)) + ); +}; diff --git a/lib/tasks/taskRepository.js b/lib/tasks/taskRepository.js index 278c4dd40..aa45af249 100644 --- a/lib/tasks/taskRepository.js +++ b/lib/tasks/taskRepository.js @@ -14,6 +14,7 @@ const taskInfos = { generateManifestBundle: {path: "./bundlers/generateManifestBundle"}, generateFlexChangesBundle: {path: "./bundlers/generateFlexChangesBundle"}, generateComponentPreload: {path: "./bundlers/generateComponentPreload"}, + generateResourcesJson: {path: "./generateResourcesJson"}, generateStandaloneAppBundle: {path: "./bundlers/generateStandaloneAppBundle"}, generateBundle: {path: "./bundlers/generateBundle"}, generateLibraryPreload: {path: "./bundlers/generateLibraryPreload"}, diff --git a/lib/types/application/ApplicationBuilder.js b/lib/types/application/ApplicationBuilder.js index 7ff80869b..ec5d11358 100644 --- a/lib/types/application/ApplicationBuilder.js +++ b/lib/types/application/ApplicationBuilder.js @@ -187,6 +187,16 @@ class ApplicationBuilder extends AbstractBuilder { } }); }); + + this.addTask("generateResourcesJson", () => { + return getTask("generateResourcesJson").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + options: { + projectName: project.metadata.name + } + }); + }); } } diff --git a/lib/types/library/LibraryBuilder.js b/lib/types/library/LibraryBuilder.js index a5b4935a4..9e1833037 100644 --- a/lib/types/library/LibraryBuilder.js +++ b/lib/types/library/LibraryBuilder.js @@ -177,6 +177,16 @@ class LibraryBuilder extends AbstractBuilder { } }); }); + + this.addTask("generateResourcesJson", () => { + return getTask("generateResourcesJson").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + options: { + projectName: project.metadata.name + } + }); + }); } } diff --git a/lib/types/themeLibrary/ThemeLibraryBuilder.js b/lib/types/themeLibrary/ThemeLibraryBuilder.js index 29baabb31..b1b8799a5 100644 --- a/lib/types/themeLibrary/ThemeLibraryBuilder.js +++ b/lib/types/themeLibrary/ThemeLibraryBuilder.js @@ -35,6 +35,16 @@ class ThemeLibraryBuilder extends AbstractBuilder { } }); }); + + this.addTask("generateResourcesJson", () => { + return getTask("generateResourcesJson").task({ + workspace: resourceCollections.workspace, + dependencies: resourceCollections.dependencies, + options: { + projectName: project.metadata.name + } + }); + }); } } diff --git a/test/expected/build/application.j/dest-resources-json/Component-preload.js b/test/expected/build/application.j/dest-resources-json/Component-preload.js new file mode 100644 index 000000000..4e58fdf09 --- /dev/null +++ b/test/expected/build/application.j/dest-resources-json/Component-preload.js @@ -0,0 +1,12 @@ +//@ui5-bundle application/j/Component-preload.js +jQuery.sap.registerPreloadedModules({ +"version":"2.0", +"modules":{ + "application/j/Component.js":function(){sap.ui.define(["sap/ui/core/UIComponent"],function(n){"use strict";return n.extend("application.j.Component",{metadata:{manifest:"json"}})}); +}, + "application/j/changes/coding/MyExtension.js":function(){sap.ui.define([],function(){return{}}); +}, + "application/j/changes/flexibility-bundle.json":'{"changes":[{"fileName":"id_456_addField","fileType":"change","changeType":"hideControl","component":"application.j.Component","content":{},"selector":{"id":"control1"},"layer":"VENDOR","texts":{},"namespace":"apps/application.j.Component/changes","creation":"2023-10-30T13:52:40.4754350Z","originalLanguage":"","conditions":{},"support":{"generator":"did it","user":"Max Mustermann"}},{"fileName":"id_123_addField","fileType":"change","changeType":"hideControl","component":"application.j.Component","content":{},"selector":{"id":"control1"},"layer":"VENDOR","texts":{},"namespace":"apps/application.j.Component/changes","creation":"2025-10-30T13:52:40.4754350Z","originalLanguage":"","conditions":{},"support":{"generator":"did it","user":"Max Mustermann"}}],"compVariants":[{"fileName":"id_111_compVariants","fileType":"variant","changeType":"hideControl","component":"application.j.Component","content":{},"selector":{"id":"control1"},"layer":"VENDOR","texts":{},"namespace":"apps/application.j.Component/changes","creation":"2025-10-30T13:52:40.4754350Z","originalLanguage":"","conditions":{},"support":{"generator":"did it","user":"Max Mustermann"},"appDescriptorChange":false}],"variants":[{"fileName":"id_111_test","fileType":"ctrl_variant","changeType":"hideControl","component":"application.j.Component","content":{},"selector":{"id":"control1"},"layer":"VENDOR","texts":{},"namespace":"apps/application.j.Component/changes","creation":"2025-10-30T13:52:40.4754350Z","originalLanguage":"","conditions":{},"support":{"generator":"did it","user":"Max Mustermann"}}],"variantChanges":[{"fileName":"id_111_test","fileType":"ctrl_variant_change","changeType":"hideControl","component":"application.j.Component","content":{},"selector":{"id":"control1"},"layer":"VENDOR","texts":{},"namespace":"apps/application.j.Component/changes","creation":"2025-10-30T13:52:40.4754350Z","originalLanguage":"","conditions":{},"support":{"generator":"did it","user":"Max Mustermann"}}],"variantDependentControlChanges":[{"fileName":"id_111_variantDependentControlChange","fileType":"change","changeType":"hideControl","component":"application.j.Component","content":{},"selector":{"id":"control1"},"layer":"VENDOR","texts":{},"namespace":"apps/application.j.Component/changes","creation":"2025-10-30T13:52:40.4754350Z","originalLanguage":"","conditions":{},"support":{"generator":"did it","user":"Max Mustermann"},"variantReference":"someting here"}],"variantManagementChanges":[{"fileName":"id_111_test","fileType":"ctrl_variant_management_change","changeType":"hideControl","component":"application.j.Component","content":{},"selector":{"id":"control1"},"layer":"VENDOR","texts":{},"namespace":"apps/application.j.Component/changes","creation":"2025-10-30T13:52:40.4754350Z","originalLanguage":"","conditions":{},"support":{"generator":"did it","user":"Max Mustermann"}}]}', + "application/j/changes/fragments/MyFragment.fragment.xml":'', + "application/j/manifest.json":'{"_version":"1.1.0","sap.app":{"_version":"1.1.0","id":"application.j","type":"application","applicationVersion":{"version":"1.2.2"},"embeds":["embedded"],"title":"{{title}}"},"sap.ui5":{"dependencies":{"minUI5Version":"1.73.2","libs":{"sap.ui.layout":{},"sap.ui.core":{},"sap.m":{},"sap.ui.fl":{"lazy":false}}}}}' +}}); diff --git a/test/expected/build/application.j/dest-resources-json/Component.js b/test/expected/build/application.j/dest-resources-json/Component.js new file mode 100644 index 000000000..984f96eaf --- /dev/null +++ b/test/expected/build/application.j/dest-resources-json/Component.js @@ -0,0 +1 @@ +sap.ui.define(["sap/ui/core/UIComponent"],function(n){"use strict";return n.extend("application.j.Component",{metadata:{manifest:"json"}})}); \ No newline at end of file diff --git a/test/expected/build/application.j/dest-resources-json/changes/coding/MyExtension.js b/test/expected/build/application.j/dest-resources-json/changes/coding/MyExtension.js new file mode 100644 index 000000000..b9e475d8e --- /dev/null +++ b/test/expected/build/application.j/dest-resources-json/changes/coding/MyExtension.js @@ -0,0 +1 @@ +sap.ui.define([],function(){return{}}); \ No newline at end of file diff --git a/test/expected/build/application.j/dest-resources-json/changes/flexibility-bundle.json b/test/expected/build/application.j/dest-resources-json/changes/flexibility-bundle.json new file mode 100644 index 000000000..b3a5c5b1e --- /dev/null +++ b/test/expected/build/application.j/dest-resources-json/changes/flexibility-bundle.json @@ -0,0 +1 @@ +{"changes":[{"fileName":"id_456_addField","fileType":"change","changeType":"hideControl","component":"application.j.Component","content":{},"selector":{"id":"control1"},"layer":"VENDOR","texts":{},"namespace":"apps/application.j.Component/changes","creation":"2023-10-30T13:52:40.4754350Z","originalLanguage":"","conditions":{},"support":{"generator":"did it","user":"Max Mustermann"}},{"fileName":"id_123_addField","fileType":"change","changeType":"hideControl","component":"application.j.Component","content":{},"selector":{"id":"control1"},"layer":"VENDOR","texts":{},"namespace":"apps/application.j.Component/changes","creation":"2025-10-30T13:52:40.4754350Z","originalLanguage":"","conditions":{},"support":{"generator":"did it","user":"Max Mustermann"}}],"compVariants":[{"fileName":"id_111_compVariants","fileType":"variant","changeType":"hideControl","component":"application.j.Component","content":{},"selector":{"id":"control1"},"layer":"VENDOR","texts":{},"namespace":"apps/application.j.Component/changes","creation":"2025-10-30T13:52:40.4754350Z","originalLanguage":"","conditions":{},"support":{"generator":"did it","user":"Max Mustermann"},"appDescriptorChange":false}],"variants":[{"fileName":"id_111_test","fileType":"ctrl_variant","changeType":"hideControl","component":"application.j.Component","content":{},"selector":{"id":"control1"},"layer":"VENDOR","texts":{},"namespace":"apps/application.j.Component/changes","creation":"2025-10-30T13:52:40.4754350Z","originalLanguage":"","conditions":{},"support":{"generator":"did it","user":"Max Mustermann"}}],"variantChanges":[{"fileName":"id_111_test","fileType":"ctrl_variant_change","changeType":"hideControl","component":"application.j.Component","content":{},"selector":{"id":"control1"},"layer":"VENDOR","texts":{},"namespace":"apps/application.j.Component/changes","creation":"2025-10-30T13:52:40.4754350Z","originalLanguage":"","conditions":{},"support":{"generator":"did it","user":"Max Mustermann"}}],"variantDependentControlChanges":[{"fileName":"id_111_variantDependentControlChange","fileType":"change","changeType":"hideControl","component":"application.j.Component","content":{},"selector":{"id":"control1"},"layer":"VENDOR","texts":{},"namespace":"apps/application.j.Component/changes","creation":"2025-10-30T13:52:40.4754350Z","originalLanguage":"","conditions":{},"support":{"generator":"did it","user":"Max Mustermann"},"variantReference":"someting here"}],"variantManagementChanges":[{"fileName":"id_111_test","fileType":"ctrl_variant_management_change","changeType":"hideControl","component":"application.j.Component","content":{},"selector":{"id":"control1"},"layer":"VENDOR","texts":{},"namespace":"apps/application.j.Component/changes","creation":"2025-10-30T13:52:40.4754350Z","originalLanguage":"","conditions":{},"support":{"generator":"did it","user":"Max Mustermann"}}]} \ No newline at end of file diff --git a/test/expected/build/application.j/dest-resources-json/changes/fragments/MyFragment.fragment.xml b/test/expected/build/application.j/dest-resources-json/changes/fragments/MyFragment.fragment.xml new file mode 100644 index 000000000..39ce6859b --- /dev/null +++ b/test/expected/build/application.j/dest-resources-json/changes/fragments/MyFragment.fragment.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/test/expected/build/application.j/dest-resources-json/manifest.json b/test/expected/build/application.j/dest-resources-json/manifest.json new file mode 100644 index 000000000..f82703903 --- /dev/null +++ b/test/expected/build/application.j/dest-resources-json/manifest.json @@ -0,0 +1,28 @@ +{ + "_version": "1.1.0", + "sap.app": { + "_version": "1.1.0", + "id": "application.j", + "type": "application", + "applicationVersion": { + "version": "1.2.2" + }, + "embeds": [ + "embedded" + ], + "title": "{{title}}" + }, + "sap.ui5": { + "dependencies": { + "minUI5Version": "1.73.2", + "libs": { + "sap.ui.layout": {}, + "sap.ui.core": {}, + "sap.m": {}, + "sap.ui.fl": { + "lazy": false + } + } + } + } +} \ No newline at end of file diff --git a/test/expected/build/application.j/dest-resources-json/resources.json b/test/expected/build/application.j/dest-resources-json/resources.json new file mode 100644 index 000000000..6cc8eb8a7 --- /dev/null +++ b/test/expected/build/application.j/dest-resources-json/resources.json @@ -0,0 +1,86 @@ +{ + "_version": "1.1.0", + "resources": [ + { + "name": "Component-preload.js", + "module": "application/j/Component-preload.js", + "size": 3746, + "merged": true, + "included": [ + "application/j/Component.js", + "application/j/changes/coding/MyExtension.js", + "application/j/changes/flexibility-bundle.json", + "application/j/changes/fragments/MyFragment.fragment.xml", + "application/j/manifest.json" + ] + }, + { + "name": "Component.js", + "module": "application/j/Component.js", + "size": 141, + "required": [ + "sap/m/library.js", + "sap/ui/core/UIComponent.js", + "sap/ui/core/library.js", + "sap/ui/fl/library.js", + "sap/ui/layout/library.js" + ] + }, + { + "name": "changes/coding/MyExtension.js", + "module": "application/j/changes/coding/MyExtension.js", + "size": 39 + }, + { + "name": "changes/flexibility-bundle.json", + "module": "application/j/changes/flexibility-bundle.json", + "size": 2864 + }, + { + "name": "changes/fragments/MyFragment.fragment.xml", + "module": "application/j/changes/fragments/MyFragment.fragment.xml", + "size": 13 + }, + { + "name": "changes/id_111_appDescriptor.change", + "size": 464 + }, + { + "name": "changes/id_111_compVariants.variant", + "size": 466 + }, + { + "name": "changes/id_111_test.ctrl_variant", + "size": 432 + }, + { + "name": "changes/id_111_test.ctrl_variant_change", + "size": 439 + }, + { + "name": "changes/id_111_test.ctrl_variant_management_change", + "size": 450 + }, + { + "name": "changes/id_111_variantDependentControlChange.change", + "size": 489 + }, + { + "name": "changes/id_123_addField.change", + "size": 430 + }, + { + "name": "changes/id_456_addField.change", + "size": 430 + }, + { + "name": "manifest.json", + "module": "application/j/manifest.json", + "size": 423 + }, + { + "name": "resources.json", + "size": 1870 + } + ] +} \ No newline at end of file diff --git a/test/expected/build/application.j/dest-resources-json/resources/sap-ui-version.json b/test/expected/build/application.j/dest-resources-json/resources/sap-ui-version.json new file mode 100644 index 000000000..551fc7d92 --- /dev/null +++ b/test/expected/build/application.j/dest-resources-json/resources/sap-ui-version.json @@ -0,0 +1,7 @@ +{ + "name": "application.j", + "version": "1.0.0", + "buildTimestamp": "202008120917", + "scmRevision": "", + "libraries": [] +} \ No newline at end of file diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/.library b/test/expected/build/library.h/dest-resources-json/resources/library/h/.library new file mode 100644 index 000000000..5bdab13ab --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/.library @@ -0,0 +1,19 @@ + + + + library.h + SAP SE + Some fancy copyright + 1.0.0 + + Library D + + + + + + + + + + diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/components/Component-preload.js b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/Component-preload.js new file mode 100644 index 000000000..f66daec7d --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/Component-preload.js @@ -0,0 +1,10 @@ +//@ui5-bundle library/h/components/Component-preload.js +sap.ui.require.preload({ + "library/h/components/Component.js":function(){sap.ui.define(["sap/ui/core/UIComponent"],function(n){"use strict";return n.extend("application.g.Component",{})}); +}, + "library/h/components/TodoComponent.js":function(){/*! + * Some fancy copyright + */ +console.log(" File "); +} +}); diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/components/Component.js b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/Component.js new file mode 100644 index 000000000..422a97071 --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/Component.js @@ -0,0 +1 @@ +sap.ui.define(["sap/ui/core/UIComponent"],function(n){"use strict";return n.extend("application.g.Component",{})}); \ No newline at end of file diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/components/TodoComponent.js b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/TodoComponent.js new file mode 100644 index 000000000..bcf866e67 --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/TodoComponent.js @@ -0,0 +1,4 @@ +/*! + * Some fancy copyright + */ +console.log(" File "); \ No newline at end of file diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/components/resources.json b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/resources.json new file mode 100644 index 000000000..a1dda2e2d --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/resources.json @@ -0,0 +1,84 @@ +{ + "_version": "1.1.0", + "resources": [ + { + "name": "Component-preload.js", + "module": "library/h/components/Component-preload.js", + "size": 361, + "merged": true, + "included": [ + "library/h/components/Component.js", + "library/h/components/TodoComponent.js" + ] + }, + { + "name": "Component.js", + "module": "library/h/components/Component.js", + "size": 115, + "required": [ + "sap/ui/core/UIComponent.js" + ] + }, + { + "name": "TodoComponent.js", + "module": "library/h/components/TodoComponent.js", + "size": 54, + "format": "raw" + }, + { + "name": "resources.json", + "size": 1904 + }, + { + "name": "subcomponent1/Component-preload.js", + "module": "library/h/components/subcomponent1/Component-preload.js", + "size": 279, + "merged": true, + "included": [ + "library/h/components/subcomponent1/Component.js" + ] + }, + { + "name": "subcomponent1/Component.js", + "module": "library/h/components/subcomponent1/Component.js", + "size": 115, + "required": [ + "sap/ui/core/UIComponent.js" + ] + }, + { + "name": "subcomponent2/Component-preload.js", + "module": "library/h/components/subcomponent2/Component-preload.js", + "size": 279, + "merged": true, + "included": [ + "library/h/components/subcomponent2/Component.js" + ] + }, + { + "name": "subcomponent2/Component.js", + "module": "library/h/components/subcomponent2/Component.js", + "size": 115, + "required": [ + "sap/ui/core/UIComponent.js" + ] + }, + { + "name": "subcomponent3/Component-preload.js", + "module": "library/h/components/subcomponent3/Component-preload.js", + "size": 279, + "merged": true, + "included": [ + "library/h/components/subcomponent3/Component.js" + ] + }, + { + "name": "subcomponent3/Component.js", + "module": "library/h/components/subcomponent3/Component.js", + "size": 115, + "required": [ + "sap/ui/core/UIComponent.js" + ] + } + ] +} \ No newline at end of file diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent1/Component-preload.js b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent1/Component-preload.js new file mode 100644 index 000000000..d2309dcdf --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent1/Component-preload.js @@ -0,0 +1,5 @@ +//@ui5-bundle library/h/components/subcomponent1/Component-preload.js +sap.ui.require.preload({ + "library/h/components/subcomponent1/Component.js":function(){sap.ui.define(["sap/ui/core/UIComponent"],function(n){"use strict";return n.extend("application.g.Component",{})}); +} +}); diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent1/Component.js b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent1/Component.js new file mode 100644 index 000000000..422a97071 --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent1/Component.js @@ -0,0 +1 @@ +sap.ui.define(["sap/ui/core/UIComponent"],function(n){"use strict";return n.extend("application.g.Component",{})}); \ No newline at end of file diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent1/resources.json b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent1/resources.json new file mode 100644 index 000000000..5de7b8334 --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent1/resources.json @@ -0,0 +1,26 @@ +{ + "_version": "1.1.0", + "resources": [ + { + "name": "Component-preload.js", + "module": "library/h/components/subcomponent1/Component-preload.js", + "size": 279, + "merged": true, + "included": [ + "library/h/components/subcomponent1/Component.js" + ] + }, + { + "name": "Component.js", + "module": "library/h/components/subcomponent1/Component.js", + "size": 115, + "required": [ + "sap/ui/core/UIComponent.js" + ] + }, + { + "name": "resources.json", + "size": 494 + } + ] +} \ No newline at end of file diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent2/Component-preload.js b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent2/Component-preload.js new file mode 100644 index 000000000..ce88226b9 --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent2/Component-preload.js @@ -0,0 +1,5 @@ +//@ui5-bundle library/h/components/subcomponent2/Component-preload.js +sap.ui.require.preload({ + "library/h/components/subcomponent2/Component.js":function(){sap.ui.define(["sap/ui/core/UIComponent"],function(n){"use strict";return n.extend("application.g.Component",{})}); +} +}); diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent2/Component.js b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent2/Component.js new file mode 100644 index 000000000..422a97071 --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent2/Component.js @@ -0,0 +1 @@ +sap.ui.define(["sap/ui/core/UIComponent"],function(n){"use strict";return n.extend("application.g.Component",{})}); \ No newline at end of file diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent2/resources.json b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent2/resources.json new file mode 100644 index 000000000..ba1172a4c --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent2/resources.json @@ -0,0 +1,26 @@ +{ + "_version": "1.1.0", + "resources": [ + { + "name": "Component-preload.js", + "module": "library/h/components/subcomponent2/Component-preload.js", + "size": 279, + "merged": true, + "included": [ + "library/h/components/subcomponent2/Component.js" + ] + }, + { + "name": "Component.js", + "module": "library/h/components/subcomponent2/Component.js", + "size": 115, + "required": [ + "sap/ui/core/UIComponent.js" + ] + }, + { + "name": "resources.json", + "size": 494 + } + ] +} \ No newline at end of file diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent3/Component-preload.js b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent3/Component-preload.js new file mode 100644 index 000000000..4b88fc476 --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent3/Component-preload.js @@ -0,0 +1,5 @@ +//@ui5-bundle library/h/components/subcomponent3/Component-preload.js +sap.ui.require.preload({ + "library/h/components/subcomponent3/Component.js":function(){sap.ui.define(["sap/ui/core/UIComponent"],function(n){"use strict";return n.extend("application.g.Component",{})}); +} +}); diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent3/Component.js b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent3/Component.js new file mode 100644 index 000000000..422a97071 --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent3/Component.js @@ -0,0 +1 @@ +sap.ui.define(["sap/ui/core/UIComponent"],function(n){"use strict";return n.extend("application.g.Component",{})}); \ No newline at end of file diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent3/resources.json b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent3/resources.json new file mode 100644 index 000000000..b24c48464 --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/components/subcomponent3/resources.json @@ -0,0 +1,26 @@ +{ + "_version": "1.1.0", + "resources": [ + { + "name": "Component-preload.js", + "module": "library/h/components/subcomponent3/Component-preload.js", + "size": 279, + "merged": true, + "included": [ + "library/h/components/subcomponent3/Component.js" + ] + }, + { + "name": "Component.js", + "module": "library/h/components/subcomponent3/Component.js", + "size": 115, + "required": [ + "sap/ui/core/UIComponent.js" + ] + }, + { + "name": "resources.json", + "size": 494 + } + ] +} \ No newline at end of file diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/customBundle.js b/test/expected/build/library.h/dest-resources-json/resources/library/h/customBundle.js new file mode 100644 index 000000000..e9e0e00ed --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/customBundle.js @@ -0,0 +1,24 @@ +//@ui5-bundle library/h/customBundle.js +sap.ui.require.preload({ + "library/h/file.js":function(){/*! + * Some fancy copyright + */ +console.log(" File "); +}, + "library/h/library.js":function(){/*! + * Some fancy copyright + */ +console.log(" Library "); +}, + "library/h/some.js":function(){/*! + * Some fancy copyright + */ +//@ui5-bundle-raw-include library/h/other.js +console.log(" Some "); +} +}); +//@ui5-bundle-raw-include library/h/not.js +/*! + * Some fancy copyright + */ +console.log(" Not including "); \ No newline at end of file diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/designtime/library.designtime.js b/test/expected/build/library.h/dest-resources-json/resources/library/h/designtime/library.designtime.js new file mode 100644 index 000000000..9b135171d --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/designtime/library.designtime.js @@ -0,0 +1,4 @@ +/*! + * Some fancy copyright + */ +var myexport=function(){"use strict";String("asd")}(); \ No newline at end of file diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/file.js b/test/expected/build/library.h/dest-resources-json/resources/library/h/file.js new file mode 100644 index 000000000..bcf866e67 --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/file.js @@ -0,0 +1,4 @@ +/*! + * Some fancy copyright + */ +console.log(" File "); \ No newline at end of file diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/i18n/messagebundle.properties b/test/expected/build/library.h/dest-resources-json/resources/library/h/i18n/messagebundle.properties new file mode 100644 index 000000000..1c6a1a9e8 --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/i18n/messagebundle.properties @@ -0,0 +1 @@ +a=b \ No newline at end of file diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/i18n/messagebundle_en.properties b/test/expected/build/library.h/dest-resources-json/resources/library/h/i18n/messagebundle_en.properties new file mode 100644 index 000000000..1c6a1a9e8 --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/i18n/messagebundle_en.properties @@ -0,0 +1 @@ +a=b \ No newline at end of file diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/library.js b/test/expected/build/library.h/dest-resources-json/resources/library/h/library.js new file mode 100644 index 000000000..6900e2218 --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/library.js @@ -0,0 +1,4 @@ +/*! + * Some fancy copyright + */ +console.log(" Library "); \ No newline at end of file diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/manifest.json b/test/expected/build/library.h/dest-resources-json/resources/library/h/manifest.json new file mode 100644 index 000000000..2e67ab58d --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/manifest.json @@ -0,0 +1,39 @@ +{ + "_version": "1.9.0", + "sap.app": { + "id": "library.h", + "type": "library", + "embeds": [ + "components", + "components/subcomponent1", + "components/subcomponent2", + "components/subcomponent3" + ], + "applicationVersion": { + "version": "1.0.0" + }, + "title": "Library D", + "description": "Library D", + "resources": "resources.json", + "offline": true + }, + "sap.ui": { + "technology": "UI5", + "supportedThemes": [] + }, + "sap.ui5": { + "dependencies": { + "minUI5Version": "1.0", + "libs": {} + }, + "library": { + "i18n": false, + "content": { + "controls": [], + "elements": [], + "types": [], + "interfaces": [] + } + } + } +} \ No newline at end of file diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/not.js b/test/expected/build/library.h/dest-resources-json/resources/library/h/not.js new file mode 100644 index 000000000..c249a10c8 --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/not.js @@ -0,0 +1,4 @@ +/*! + * Some fancy copyright + */ +console.log(" Not including "); \ No newline at end of file diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/resources.json b/test/expected/build/library.h/dest-resources-json/resources/library/h/resources.json new file mode 100644 index 000000000..d0820dcf4 --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/resources.json @@ -0,0 +1,159 @@ +{ + "_version": "1.1.0", + "resources": [ + { + "name": ".library", + "size": 473 + }, + { + "name": "components/Component-preload.js", + "module": "library/h/components/Component-preload.js", + "size": 361, + "merged": true, + "included": [ + "library/h/components/Component.js", + "library/h/components/TodoComponent.js" + ] + }, + { + "name": "components/Component.js", + "module": "library/h/components/Component.js", + "size": 115, + "required": [ + "sap/ui/core/UIComponent.js" + ] + }, + { + "name": "components/TodoComponent.js", + "module": "library/h/components/TodoComponent.js", + "size": 54, + "format": "raw" + }, + { + "name": "components/subcomponent1/Component-preload.js", + "module": "library/h/components/subcomponent1/Component-preload.js", + "size": 279, + "merged": true, + "included": [ + "library/h/components/subcomponent1/Component.js" + ] + }, + { + "name": "components/subcomponent1/Component.js", + "module": "library/h/components/subcomponent1/Component.js", + "size": 115, + "required": [ + "sap/ui/core/UIComponent.js" + ] + }, + { + "name": "components/subcomponent2/Component-preload.js", + "module": "library/h/components/subcomponent2/Component-preload.js", + "size": 279, + "merged": true, + "included": [ + "library/h/components/subcomponent2/Component.js" + ] + }, + { + "name": "components/subcomponent2/Component.js", + "module": "library/h/components/subcomponent2/Component.js", + "size": 115, + "required": [ + "sap/ui/core/UIComponent.js" + ] + }, + { + "name": "components/subcomponent3/Component-preload.js", + "module": "library/h/components/subcomponent3/Component-preload.js", + "size": 279, + "merged": true, + "included": [ + "library/h/components/subcomponent3/Component.js" + ] + }, + { + "name": "components/subcomponent3/Component.js", + "module": "library/h/components/subcomponent3/Component.js", + "size": 115, + "required": [ + "sap/ui/core/UIComponent.js" + ] + }, + { + "name": "customBundle.js", + "module": "library/h/customBundle.js", + "size": 495, + "merged": true, + "included": [ + "library/h/file.js", + "library/h/library.js", + "library/h/some.js", + "library/h/other.js", + "library/h/not.js" + ] + }, + { + "name": "designtime/library.designtime.js", + "module": "library/h/designtime/library.designtime.js", + "size": 86, + "requiresTopLevelScope": true, + "exposedGlobalNames": [ + "myexport" + ], + "format": "raw", + "designtime": true + }, + { + "name": "file.js", + "module": "library/h/file.js", + "size": 54, + "format": "raw" + }, + { + "name": "i18n/messagebundle.properties", + "module": "library/h/i18n/messagebundle.properties", + "size": 3, + "locale": "", + "raw": "i18n/messagebundle.properties" + }, + { + "name": "i18n/messagebundle_en.properties", + "module": "library/h/i18n/messagebundle_en.properties", + "size": 3, + "locale": "en", + "raw": "i18n/messagebundle.properties" + }, + { + "name": "library.js", + "module": "library/h/library.js", + "size": 57, + "format": "raw" + }, + { + "name": "manifest.json", + "module": "library/h/manifest.json", + "size": 739 + }, + { + "name": "not.js", + "module": "library/h/not.js", + "size": 63, + "format": "raw" + }, + { + "name": "resources.json", + "size": 3500 + }, + { + "name": "some.js", + "module": "library/h/some.js", + "size": 99, + "format": "raw", + "merged": true, + "included": [ + "library/h/other.js" + ] + } + ] +} \ No newline at end of file diff --git a/test/expected/build/library.h/dest-resources-json/resources/library/h/some.js b/test/expected/build/library.h/dest-resources-json/resources/library/h/some.js new file mode 100644 index 000000000..1e86f9afc --- /dev/null +++ b/test/expected/build/library.h/dest-resources-json/resources/library/h/some.js @@ -0,0 +1,5 @@ +/*! + * Some fancy copyright + */ +//@ui5-bundle-raw-include library/h/other.js +console.log(" Some "); \ No newline at end of file diff --git a/test/expected/build/library.h/dest-resources-json/test-resources/library/d/Test.html b/test/expected/build/library.h/dest-resources-json/test-resources/library/d/Test.html new file mode 100644 index 000000000..e69de29bb diff --git a/test/expected/build/library.h/dest/resources/library/h/.library b/test/expected/build/library.h/dest/resources/library/h/.library index 2b110f205..5bdab13ab 100644 --- a/test/expected/build/library.h/dest/resources/library/h/.library +++ b/test/expected/build/library.h/dest/resources/library/h/.library @@ -7,5 +7,13 @@ 1.0.0 Library D + + + + + + + + diff --git a/test/expected/build/library.h/dest/resources/library/h/customBundle.js b/test/expected/build/library.h/dest/resources/library/h/customBundle.js index 21db087bb..e9e0e00ed 100644 --- a/test/expected/build/library.h/dest/resources/library/h/customBundle.js +++ b/test/expected/build/library.h/dest/resources/library/h/customBundle.js @@ -13,6 +13,12 @@ console.log(" Library "); "library/h/some.js":function(){/*! * Some fancy copyright */ +//@ui5-bundle-raw-include library/h/other.js console.log(" Some "); } }); +//@ui5-bundle-raw-include library/h/not.js +/*! + * Some fancy copyright + */ +console.log(" Not including "); \ No newline at end of file diff --git a/test/expected/build/library.h/dest/resources/library/h/designtime/library.designtime.js b/test/expected/build/library.h/dest/resources/library/h/designtime/library.designtime.js new file mode 100644 index 000000000..9b135171d --- /dev/null +++ b/test/expected/build/library.h/dest/resources/library/h/designtime/library.designtime.js @@ -0,0 +1,4 @@ +/*! + * Some fancy copyright + */ +var myexport=function(){"use strict";String("asd")}(); \ No newline at end of file diff --git a/test/expected/build/library.h/dest/resources/library/h/i18n/messagebundle.properties b/test/expected/build/library.h/dest/resources/library/h/i18n/messagebundle.properties new file mode 100644 index 000000000..1c6a1a9e8 --- /dev/null +++ b/test/expected/build/library.h/dest/resources/library/h/i18n/messagebundle.properties @@ -0,0 +1 @@ +a=b \ No newline at end of file diff --git a/test/expected/build/library.h/dest/resources/library/h/i18n/messagebundle_en.properties b/test/expected/build/library.h/dest/resources/library/h/i18n/messagebundle_en.properties new file mode 100644 index 000000000..1c6a1a9e8 --- /dev/null +++ b/test/expected/build/library.h/dest/resources/library/h/i18n/messagebundle_en.properties @@ -0,0 +1 @@ +a=b \ No newline at end of file diff --git a/test/expected/build/library.h/dest/resources/library/h/some.js b/test/expected/build/library.h/dest/resources/library/h/some.js index 98e1e6665..1e86f9afc 100644 --- a/test/expected/build/library.h/dest/resources/library/h/some.js +++ b/test/expected/build/library.h/dest/resources/library/h/some.js @@ -1,4 +1,5 @@ /*! * Some fancy copyright */ +//@ui5-bundle-raw-include library/h/other.js console.log(" Some "); \ No newline at end of file diff --git a/test/expected/build/theme.j/dest-resources-json/resources/theme/j/themes/somefancytheme/Button.less b/test/expected/build/theme.j/dest-resources-json/resources/theme/j/themes/somefancytheme/Button.less new file mode 100644 index 000000000..ca968183f --- /dev/null +++ b/test/expected/build/theme.j/dest-resources-json/resources/theme/j/themes/somefancytheme/Button.less @@ -0,0 +1,3 @@ +.someClass { + color: @someColor +} diff --git a/test/expected/build/theme.j/dest-resources-json/resources/theme/j/themes/somefancytheme/library-RTL.css b/test/expected/build/theme.j/dest-resources-json/resources/theme/j/themes/somefancytheme/library-RTL.css new file mode 100644 index 000000000..5009ca50e --- /dev/null +++ b/test/expected/build/theme.j/dest-resources-json/resources/theme/j/themes/somefancytheme/library-RTL.css @@ -0,0 +1,3 @@ +.someClass{color:#000} +/* Inline theming parameters */ +#sap-ui-theme-theme\.j{background-image:url('data:text/plain;utf-8,%7B%22someColor%22%3A%22%23000%22%7D')} diff --git a/test/expected/build/theme.j/dest-resources-json/resources/theme/j/themes/somefancytheme/library-parameters.json b/test/expected/build/theme.j/dest-resources-json/resources/theme/j/themes/somefancytheme/library-parameters.json new file mode 100644 index 000000000..a190cda03 --- /dev/null +++ b/test/expected/build/theme.j/dest-resources-json/resources/theme/j/themes/somefancytheme/library-parameters.json @@ -0,0 +1 @@ +{"someColor":"#000"} \ No newline at end of file diff --git a/test/expected/build/theme.j/dest-resources-json/resources/theme/j/themes/somefancytheme/library.css b/test/expected/build/theme.j/dest-resources-json/resources/theme/j/themes/somefancytheme/library.css new file mode 100644 index 000000000..5009ca50e --- /dev/null +++ b/test/expected/build/theme.j/dest-resources-json/resources/theme/j/themes/somefancytheme/library.css @@ -0,0 +1,3 @@ +.someClass{color:#000} +/* Inline theming parameters */ +#sap-ui-theme-theme\.j{background-image:url('data:text/plain;utf-8,%7B%22someColor%22%3A%22%23000%22%7D')} diff --git a/test/expected/build/theme.j/dest-resources-json/resources/theme/j/themes/somefancytheme/library.source.less b/test/expected/build/theme.j/dest-resources-json/resources/theme/j/themes/somefancytheme/library.source.less new file mode 100644 index 000000000..834de919e --- /dev/null +++ b/test/expected/build/theme.j/dest-resources-json/resources/theme/j/themes/somefancytheme/library.source.less @@ -0,0 +1,2 @@ +@someColor: black; +@import "Button.less"; diff --git a/test/expected/build/theme.j/dest-resources-json/resources/theme/j/themes/somefancytheme/resources.json b/test/expected/build/theme.j/dest-resources-json/resources/theme/j/themes/somefancytheme/resources.json new file mode 100644 index 000000000..d4c81a925 --- /dev/null +++ b/test/expected/build/theme.j/dest-resources-json/resources/theme/j/themes/somefancytheme/resources.json @@ -0,0 +1,37 @@ +{ + "_version": "1.1.0", + "resources": [ + { + "name": "Button.less", + "size": 34, + "designtime": true, + "theme": "somefancytheme" + }, + { + "name": "library-RTL.css", + "size": 162, + "theme": "somefancytheme" + }, + { + "name": "library-parameters.json", + "module": "theme/j/themes/somefancytheme/library-parameters.json", + "size": 20, + "theme": "somefancytheme" + }, + { + "name": "library.css", + "size": 162, + "theme": "somefancytheme" + }, + { + "name": "library.source.less", + "size": 42, + "designtime": true, + "theme": "somefancytheme" + }, + { + "name": "resources.json", + "size": 633 + } + ] +} \ No newline at end of file diff --git a/test/fixtures/library.h/main/src/library/h/.library b/test/fixtures/library.h/main/src/library/h/.library index 8d1e46775..797c61cc8 100644 --- a/test/fixtures/library.h/main/src/library/h/.library +++ b/test/fixtures/library.h/main/src/library/h/.library @@ -7,5 +7,13 @@ ${version} Library D + + + + + + + + diff --git a/test/fixtures/library.h/main/src/library/h/designtime/library.designtime.js b/test/fixtures/library.h/main/src/library/h/designtime/library.designtime.js new file mode 100644 index 000000000..e961048c1 --- /dev/null +++ b/test/fixtures/library.h/main/src/library/h/designtime/library.designtime.js @@ -0,0 +1,14 @@ +/*! + * ${copyright} + */ + +/** + * designtime and global export + */ +var myexport = (function() { + + "use strict"; + + String("asd"); + +}()); diff --git a/test/fixtures/library.h/main/src/library/h/i18n/messagebundle.properties b/test/fixtures/library.h/main/src/library/h/i18n/messagebundle.properties new file mode 100644 index 000000000..1c6a1a9e8 --- /dev/null +++ b/test/fixtures/library.h/main/src/library/h/i18n/messagebundle.properties @@ -0,0 +1 @@ +a=b \ No newline at end of file diff --git a/test/fixtures/library.h/main/src/library/h/i18n/messagebundle_en.properties b/test/fixtures/library.h/main/src/library/h/i18n/messagebundle_en.properties new file mode 100644 index 000000000..1c6a1a9e8 --- /dev/null +++ b/test/fixtures/library.h/main/src/library/h/i18n/messagebundle_en.properties @@ -0,0 +1 @@ +a=b \ No newline at end of file diff --git a/test/fixtures/library.h/main/src/library/h/some.js b/test/fixtures/library.h/main/src/library/h/some.js index f14efacd4..c1a44398b 100644 --- a/test/fixtures/library.h/main/src/library/h/some.js +++ b/test/fixtures/library.h/main/src/library/h/some.js @@ -1,4 +1,5 @@ /*! * ${copyright} */ +//@ui5-bundle-raw-include library/h/other.js console.log(' Some '); diff --git a/test/lib/builder/builder.js b/test/lib/builder/builder.js index 4c3d19821..06c27325a 100644 --- a/test/lib/builder/builder.js +++ b/test/lib/builder/builder.js @@ -8,6 +8,7 @@ const readFile = promisify(fs.readFile); const assert = chai.assert; const sinon = require("sinon"); const mock = require("mock-require"); +const resourceFactory = require("@ui5/fs").resourceFactory; const ui5Builder = require("../../../"); const builder = ui5Builder.builder; @@ -55,7 +56,7 @@ function cloneProjectTree(tree) { return clone; } -async function checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, destPath) { +async function checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath) { for (let i = 0; i < expectedFiles.length; i++) { const expectedFile = expectedFiles[i]; const relativeFile = path.relative(expectedPath, expectedFile); @@ -66,9 +67,16 @@ async function checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, des if (expectedFile.endsWith("sap-ui-cachebuster-info.json")) { currentContent = JSON.parse(currentContent.replace(/(:\s+)(\d+)/g, ": 0")); expectedContent = JSON.parse(expectedContent.replace(/(:\s+)(\d+)/g, ": 0")); - assert.deepEqual(currentContent, expectedContent); + t.deepEqual(currentContent, expectedContent); } else { - assert.equal(currentContent.replace(newLineRegexp, "\n"), expectedContent.replace(newLineRegexp, "\n")); + if (expectedFile.endsWith(".json")) { + try { + t.deepEqual(JSON.parse(currentContent), JSON.parse(expectedContent), expectedFile); + } catch (e) { + t.falsy(e, expectedFile); + } + } + t.is(currentContent.replace(newLineRegexp, "\n"), expectedContent.replace(newLineRegexp, "\n"), relativeFile); } }; await Promise.all([currentFileContentPromise, expectedFileContentPromise]).then(assertContents); @@ -177,7 +185,7 @@ test("Build application.a", (t) => { // Check for all directories and files assert.directoryDeepEqual(destPath, expectedPath); // Check for all file contents - return checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, destPath); + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); }).then(() => { t.pass(); }); @@ -208,7 +216,7 @@ test("Build application.a with dependencies", (t) => { // Check for all directories and files assert.directoryDeepEqual(destPath, expectedPath); // Check for all file contents - return checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, destPath); + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); }).then(() => { t.pass(); }); @@ -229,7 +237,7 @@ test("Build application.a with dependencies include", (t) => { // Check for all directories and files assert.directoryDeepEqual(destPath, expectedPath); // Check for all file contents - return checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, destPath); + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); }).then(() => { t.pass(); }); @@ -250,7 +258,7 @@ test("Build application.a with dependencies exclude", (t) => { // Check for all directories and files assert.directoryDeepEqual(destPath, expectedPath); // Check for all file contents - return checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, destPath); + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); }).then(() => { t.pass(); }); @@ -271,7 +279,7 @@ test("Build application.a self-contained", (t) => { // Check for all directories and files assert.directoryDeepEqual(destPath, expectedPath); // Check for all file contents - return checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, destPath); + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); }).then(() => { t.pass(); }); @@ -293,7 +301,7 @@ test("Build application.a with dependencies self-contained", (t) => { // Check for all directories and files assert.directoryDeepEqual(destPath, expectedPath); // Check for all file contents - return checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, destPath); + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); }).then(() => { t.pass(); }); @@ -314,7 +322,7 @@ test("Build application.a [dev mode]", (t) => { assert.directoryDeepEqual(destPath, expectedPath); // Check for all file contents - return checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, destPath); + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); }).then(() => { t.pass(); }); @@ -342,7 +350,7 @@ test("Build application.a and clean target path [dev mode]", (t) => { // Check for all directories and files assert.directoryDeepEqual(destPath, expectedPath); // Check for all file contents - return checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, destPath); + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); }).then(() => { t.pass(); }); @@ -362,7 +370,7 @@ test("Build application.g", (t) => { // Check for all directories and files assert.directoryDeepEqual(destPath, expectedPath); // Check for all file contents - return checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, destPath); + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); }).then(() => { t.pass(); }); @@ -382,7 +390,7 @@ test("Build application.g with component preload paths", (t) => { // Check for all directories and files assert.directoryDeepEqual(destPath, expectedPath); // Check for all file contents - return checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, destPath); + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); }).then(() => { t.pass(); }); @@ -403,7 +411,7 @@ test("Build application.g with excludes", (t) => { // Check for all directories and files assert.directoryDeepEqual(destPath, expectedPath); // Check for all file contents - return checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, destPath); + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); }).then(() => { t.pass(); }); @@ -424,7 +432,7 @@ test("Build application.h", (t) => { // Check for all directories and files assert.directoryDeepEqual(destPath, expectedPath); // Check for all file contents - return checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, destPath); + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); }).then(() => { t.pass(); }); @@ -444,7 +452,7 @@ test("Build application.i", (t) => { // Check for all directories and files assert.directoryDeepEqual(destPath, expectedPath); // Check for all file contents - return checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, destPath); + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); }).then(() => { t.pass(); }); @@ -464,7 +472,52 @@ test("Build application.j", (t) => { // Check for all directories and files assert.directoryDeepEqual(destPath, expectedPath); // Check for all file contents - return checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, destPath); + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); + }).then(() => { + t.pass(); + }); +}); + +test("Build application.j with resources.json and version info", (t) => { + const destPath = "./test/tmp/build/application.j/dest-resources-json"; + const expectedPath = path.join("test", "expected", "build", "application.j", "dest-resources-json"); + + + const dummyVersionInfoGenerator = () => { + const versionJson = { + "name": "application.j", + "version": "1.0.0", + "buildTimestamp": "202008120917", + "scmRevision": "", + "libraries": [] + }; + + return [resourceFactory.createResource({ + path: "/resources/sap-ui-version.json", + string: JSON.stringify(versionJson, null, "\t") + })]; + }; + + mock("../../../lib/processors/versionInfoGenerator", dummyVersionInfoGenerator); + mock.reRequire("../../../lib/tasks/generateVersionInfo"); + + const builder = mock.reRequire("../../../lib/builder/builder"); + + + return builder.build({ + includedTasks: [ + "generateResourcesJson" + ], + tree: applicationJTree, + destPath, + excludedTasks: ["createDebugFiles", "generateStandaloneAppBundle"] + }).then(() => { + return findFiles(expectedPath); + }).then((expectedFiles) => { + // Check for all directories and files + assert.directoryDeepEqual(destPath, expectedPath); + // Check for all file contents + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); }).then(() => { t.pass(); }); @@ -485,7 +538,7 @@ test("Build library.d with copyright from .library file", (t) => { assert.directoryDeepEqual(destPath, expectedPath); // Check for all file contents - return checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, destPath); + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); }).then(() => { t.pass(); }); @@ -506,7 +559,7 @@ test("Build library.e with copyright from settings of ui5.yaml", (t) => { assert.directoryDeepEqual(destPath, expectedPath); // Check for all file contents - return checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, destPath); + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); }).then(() => { t.pass(); }); @@ -527,7 +580,31 @@ test("Build library.h with custom bundles and component-preloads", (t) => { assert.directoryDeepEqual(destPath, expectedPath); // Check for all file contents - return checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, destPath); + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); + }).then(() => { + t.pass(); + }); +}); + +test("Build library.h with custom bundles and component-preloads with resources.json", (t) => { + const destPath = path.join("test", "tmp", "build", "library.h", "dest-resources-json"); + const expectedPath = path.join("test", "expected", "build", "library.h", "dest-resources-json"); + + return builder.build({ + includedTasks: [ + "generateResourcesJson" + ], + tree: libraryHTree, + destPath, + excludedTasks: ["createDebugFiles", "generateLibraryPreload"] + }).then(() => { + return findFiles(expectedPath); + }).then((expectedFiles) => { + // Check for all directories and files + assert.directoryDeepEqual(destPath, expectedPath); + + // Check for all file contents + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); }).then(() => { t.pass(); }); @@ -548,7 +625,7 @@ test("Build library.i with manifest info taken from .library and library.js", (t assert.directoryDeepEqual(destPath, expectedPath); // Check for all file contents - return checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, destPath); + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); }).then(() => { t.pass(); }); @@ -570,7 +647,7 @@ test("Build library.j with JSDoc build only", (t) => { assert.directoryDeepEqual(destPath, expectedPath); // Check for all file contents - return checkFileContentsIgnoreLineFeeds(expectedFiles, expectedPath, destPath); + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); }).then(() => { t.pass(); }); @@ -588,13 +665,30 @@ test("Build theme.j even without an library", (t) => { // Check for all directories and files assert.directoryDeepEqual(destPath, expectedPath); - // Check for all file contents - expectedFiles.forEach((expectedFile) => { - const relativeFile = path.relative(expectedPath, expectedFile); - const destFile = path.join(destPath, relativeFile); - assert.fileEqual(destFile, expectedFile); - t.pass(); - }); + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); + }).then(() => { + t.pass(); + }); +}); + +test("Build theme.j even without an library with resources.json", (t) => { + const destPath = "./test/tmp/build/theme.j/dest-resources-json"; + const expectedPath = "./test/expected/build/theme.j/dest-resources-json"; + return builder.build({ + includedTasks: [ + "generateResourcesJson" + ], + tree: themeJTree, + destPath + }).then(() => { + return findFiles(expectedPath); + }).then((expectedFiles) => { + // Check for all directories and files + assert.directoryDeepEqual(destPath, expectedPath); + + return checkFileContentsIgnoreLineFeeds(t, expectedFiles, expectedPath, destPath); + }).then(() => { + t.pass(); }); }); @@ -1181,11 +1275,19 @@ const libraryHTree = { "library/h/some.js", "library/h/library.js", "library/h/file.js", - "!library/h/not.js", "!library/h/components/" ], "resolve": false, "renderer": false + }, { + "mode": "raw", + "filters": [ + "library/h/not.js" + ], + "resolve": true, + "declareModules": false, + "sort": true, + "renderer": false }] }, "bundleOptions": { diff --git a/test/lib/lbt/analyzer/JSModuleAnalyzer.js b/test/lib/lbt/analyzer/JSModuleAnalyzer.js index 23a1b4be1..e08b57a6a 100644 --- a/test/lib/lbt/analyzer/JSModuleAnalyzer.js +++ b/test/lib/lbt/analyzer/JSModuleAnalyzer.js @@ -39,9 +39,7 @@ function analyze(file, name) { reject(err); } try { - const ast = esprima.parseScript(buffer.toString(), {comment: true}); - const info = new ModuleInfo(name); - new JSModuleAnalyzer().analyze(ast, name, info); + const info = analyzeString(buffer.toString(), name); resolve(info); } catch (execErr) { reject(execErr); @@ -50,6 +48,13 @@ function analyze(file, name) { }); } +function analyzeString(content, name) { + const ast = esprima.parseScript(content, {comment: true}); + const info = new ModuleInfo(name); + new JSModuleAnalyzer().analyze(ast, name, info); + return info; +} + function assertModuleNamesEqual(t, actual, expected, msg) { actual.sort(); expected.sort(); @@ -67,7 +72,8 @@ function analyzeModule( expectedDependencies, expectedConditionalDependencies, expectedSubmodules, - ignoreImplicitDependencies + ignoreImplicitDependencies, + rawModule ) { // analyze(file, name).then( (info) => { @@ -96,6 +102,13 @@ function analyzeModule( } t.false(info.dynamicDependencies, "no use of dynamic dependencies should have been detected"); + if (rawModule) { + t.true(info.rawModule, + "raw module"); + } else { + t.false(info.rawModule, + "ui5 module"); + } }).then(() => t.end(), (e) => t.fail(`failed to analyze module with error: ${e.message}`)); } @@ -114,6 +127,7 @@ test.cb("DefineWithLegacyCalls", analyzeModule, "modules/define_with_legacy_call test.cb("OldStyleModuleWithoutDeclare", function(t) { analyze("modules/no_declare_but_requires.js", null).then((info) => { t.is(info.name, null, "module name should be null"); + t.true(info.rawModule, "raw module"); assertModuleNamesEqual(t, info.dependencies, ["dependency1.js", "dependency2.js", "jquery.sap.global.js"], @@ -122,7 +136,7 @@ test.cb("OldStyleModuleWithoutDeclare", function(t) { }); }); -test.cb("NotAnUI5Module", analyzeModule, "modules/not_a_module.js", "modules/not_a_module.js", NO_DEPENDENCIES); +test.cb("NotAnUI5Module", analyzeModule, "modules/not_a_module.js", "modules/not_a_module.js", NO_DEPENDENCIES, NO_DEPENDENCIES, NO_DEPENDENCIES, NO_DEPENDENCIES, true); test.cb("AMDSpecialDependenciesShouldBeIgnored", (t) => { analyzeModule(t, @@ -239,7 +253,42 @@ test.cb("OldStyleBundle", (t) => { analyzeModule(t, "modules/bundle-oldstyle.js", "sap-ui-core.js", - [], + [ + "jquery.sap.dom.js", + "jquery.sap.script.js", + "sap/ui/base/Object.js", + "sap/ui/base/BindingParser.js", + "sap/ui/base/EventProvider.js", + "sap/ui/base/ManagedObjectMetadata.js", + "sap/ui/model/BindingMode.js", + "sap/ui/model/CompositeBinding.js", + "sap/ui/model/Context.js", + "sap/ui/model/FormatException.js", + "sap/ui/model/ListBinding.js", + "sap/ui/model/Model.js", + "sap/ui/model/ParseException.js", + "sap/ui/model/TreeBinding.js", + "sap/ui/model/Type.js", + "sap/ui/model/ValidateException.js", + "jquery.sap.strings.js", + "sap/ui/Global.js", + "sap/ui/base/Interface.js", + "sap/ui/core/Component.js", + "sap/ui/core/Configuration.js", + "sap/ui/core/Control.js", + "sap/ui/core/Element.js", + "sap/ui/core/ElementMetadata.js", + "sap/ui/core/FocusHandler.js", + "sap/ui/core/RenderManager.js", + "sap/ui/core/ResizeHandler.js", + "sap/ui/core/ThemeCheck.js", + "sap/ui/core/UIArea.js", + "sap/ui/core/message/MessageManager.js", + "jquery.sap.mobile.js", + "jquery.sap.properties.js", + "jquery.sap.resources.js", + "jquery.sap.sjax.js" + ], [], [ "sap/ui/Device.js", @@ -266,7 +315,42 @@ test.cb("OldStyleBundleV2", (t) => { analyzeModule(t, "modules/bundle-oldstyle-v2.js", "sap-ui-core.js", - [], + [ + "jquery.sap.dom.js", + "jquery.sap.script.js", + "sap/ui/base/Object.js", + "sap/ui/base/BindingParser.js", + "sap/ui/base/EventProvider.js", + "sap/ui/base/ManagedObjectMetadata.js", + "sap/ui/model/BindingMode.js", + "sap/ui/model/CompositeBinding.js", + "sap/ui/model/Context.js", + "sap/ui/model/FormatException.js", + "sap/ui/model/ListBinding.js", + "sap/ui/model/Model.js", + "sap/ui/model/ParseException.js", + "sap/ui/model/TreeBinding.js", + "sap/ui/model/Type.js", + "sap/ui/model/ValidateException.js", + "jquery.sap.strings.js", + "sap/ui/Global.js", + "sap/ui/base/Interface.js", + "sap/ui/core/Component.js", + "sap/ui/core/Configuration.js", + "sap/ui/core/Control.js", + "sap/ui/core/Element.js", + "sap/ui/core/ElementMetadata.js", + "sap/ui/core/FocusHandler.js", + "sap/ui/core/RenderManager.js", + "sap/ui/core/ResizeHandler.js", + "sap/ui/core/ThemeCheck.js", + "sap/ui/core/UIArea.js", + "sap/ui/core/message/MessageManager.js", + "jquery.sap.mobile.js", + "jquery.sap.properties.js", + "jquery.sap.resources.js", + "jquery.sap.sjax.js" + ], [], [ "sap/ui/Device.js", @@ -293,7 +377,60 @@ test.cb("EvoBundle", (t) => { analyzeModule(t, "modules/bundle-evo.js", "sap-ui-core.js", - [], + [ + "sap/base/util/now.js", + "sap/base/util/Version.js", + "sap/ui/dom/getComputedStyleFix.js", + "sap/ui/dom/activeElementFix.js", + "sap/ui/dom/includeScript.js", + "sap/ui/dom/includeStylesheet.js", + "sap/ui/core/support/Hotkeys.js", + "sap/ui/security/FrameOptions.js", + "sap/ui/performance/Measurement.js", + "sap/ui/performance/trace/Interaction.js", + "sap/ui/base/syncXHRFix.js", + "sap/base/util/LoaderExtensions.js", + "sap/base/util/defineLazyProperty.js", + "sap/base/util/ObjectPath.js", + "sap/base/util/isPlainObject.js", + "sap/ui/base/Object.js", + "sap/ui/base/BindingParser.js", + "sap/ui/base/EventProvider.js", + "sap/ui/base/ManagedObjectMetadata.js", + "sap/ui/model/BindingMode.js", + "sap/ui/model/StaticBinding.js", + "sap/ui/model/CompositeBinding.js", + "sap/ui/model/Context.js", + "sap/ui/model/FormatException.js", + "sap/ui/model/ParseException.js", + "sap/ui/model/Type.js", + "sap/ui/model/ValidateException.js", + "sap/ui/base/SyncPromise.js", + "sap/ui/util/ActivityDetection.js", + "sap/base/util/deepClone.js", + "sap/base/util/deepEqual.js", + "sap/base/util/uid.js", + "sap/ui/Global.js", + "sap/ui/base/Interface.js", + "sap/ui/core/Component.js", + "sap/ui/core/Configuration.js", + "sap/ui/core/Control.js", + "sap/ui/core/Element.js", + "sap/ui/core/ElementMetadata.js", + "sap/ui/core/FocusHandler.js", + "sap/ui/core/RenderManager.js", + "sap/ui/core/ResizeHandler.js", + "sap/ui/core/ThemeCheck.js", + "sap/ui/core/UIArea.js", + "sap/ui/core/message/MessageManager.js", + "sap/ui/dom/getScrollbarSize.js", + "sap/base/i18n/ResourceBundle.js", + "sap/base/util/array/uniqueSort.js", + "sap/ui/performance/trace/initTraces.js", + "sap/base/util/isEmptyObject.js", + "sap/base/util/each.js", + "sap/ui/events/jquery/EventSimulation.js" + ], [], [ "sap/ui/thirdparty/baseuri.js", @@ -333,6 +470,8 @@ test("Bundle", (t) => { ]; t.deepEqual(info.subModules, expected, "module dependencies should match"); t.truthy(info.dependencies.every((dep) => !info.isConditionalDependency(dep)), "none of the dependencies must be 'conditional'"); + t.false(info.rawModule, + "ui5 module"); }); }); @@ -360,6 +499,8 @@ test("ES6 Syntax", (t) => { "all dependencies other than 'conditional/*' and 'static/*' should be implicit"); t.false(info.dynamicDependencies, "no use of dynamic dependencies should have been detected"); + t.false(info.rawModule, + "ui5 module"); }); }); }); @@ -368,6 +509,8 @@ test("Dynamic import (declare/require)", (t) => { return analyze("modules/declare_dynamic_require.js").then((info) => { t.true(info.dynamicDependencies, "the use of dynamic dependencies should have been detected"); + t.false(info.rawModule, + "ui5 module"); }); }); @@ -375,6 +518,8 @@ test("Dynamic import (define/require)", (t) => { return analyze("modules/amd_dynamic_require.js").then((info) => { t.true(info.dynamicDependencies, "the use of dynamic dependencies should have been detected"); + t.false(info.rawModule, + "ui5 module"); }); }); @@ -382,7 +527,96 @@ test("Dynamic import (define/requireSync)", (t) => { return analyze("modules/amd_dynamic_require_sync.js").then((info) => { t.true(info.dynamicDependencies, "the use of dynamic dependencies should have been detected"); + t.false(info.rawModule, + "ui5 module"); }); }); +test("Nested require", (t) => { + const content = ` +(function(deps, callback) { + function doIt(array, callback) { + callback(); + } + var aArray = []; + doIt(aArray, function() { + doIt(["foo"], function() { + doIt(["bar"], function() { + // nested sap.ui.require + sap.ui.require(deps, callback); + }); + }); + }); +}([ + "my/dependency" +], function(myDep) { + console.log("done") +}));`; + const info = analyzeString(content, "modules/nestedRequire.js"); + t.true(info.rawModule, "raw module"); +}); + +test("Toplevel define", (t) => { + const content = ` +(function() { + function defineMyFile() { + sap.ui.define('def/MyFile', ['dep/myDep'], + function(myDep) { + return 47; + }); + } + + // conditional + if (!(window.sap && window.sap.ui && window.sap.ui.define)) { + var fnHandler = function() { + defineMyFile(); + }; + my.addEventListener("myevent", fnHandler); + } else { + defineMyFile(); + } +}()); `; + const info = analyzeString(content, "modules/functionDefine.js"); + t.true(info.rawModule, "raw module"); +}); + +test("Invalid ui5 bundle comment", (t) => { + const content = `/@ui5-bundles sap/ui/thirdparty/xxx.js +if(!('xxx'in Node.prototype)){} +//@ui5-bundle-raw-includes sap/ui/thirdparty/aaa.js +(function(g,f){g.AAA=f();}(this,(function(){}))); +sap.ui.define("my/module", ["sap/ui/core/UIComponent"],function(n){"use strict";return 47+n});`; + const info = analyzeString(content, "modules/bundle-evo_invalid_comment.js"); + t.is(info.name, "my/module.js", + "module name matches"); + t.deepEqual(info.subModules, [], + "no submodules"); +}); + +test("Declare two times", (t) => { + const content = `jQuery.sap.declare("sap.ui.testmodule"); +sap.ui.testmodule.load = function(modName) { + jQuery.sap.require(modName); +}; +jQuery.sap.declare("sap.ui.testmodule");`; + const info = analyzeString(content, "modules/declare_times_two.js"); + t.is(info.name, "sap/ui/testmodule.js", + "module name matches"); + t.deepEqual(info.subModules, [], + "no submodules"); +}); + +test("Declare dynamic name", (t) => { + const content = `var sCommonName = "sap.ui" +jQuery.sap.declare(sCommonName + ".testmodule"); + +sap.ui.testmodule.load = function(modName) { + jQuery.sap.require(modName); +};`; + const info = analyzeString(content, "modules/dynamic_name.js"); + t.is(info.name, "modules/dynamic_name.js", + "module name matches"); + t.deepEqual(info.subModules, [], + "no submodules"); +}); diff --git a/test/lib/lbt/resources/Resource.js b/test/lib/lbt/resources/Resource.js new file mode 100644 index 000000000..b6e0670ce --- /dev/null +++ b/test/lib/lbt/resources/Resource.js @@ -0,0 +1,33 @@ +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); + +let Resource = require("../../../../lib/lbt/resources/Resource"); + +test.serial("Resource: buffer", async (t) => { + const readFileStub = sinon.stub().callsArg(1); + mock("graceful-fs", { + readFile: readFileStub + }); + mock.reRequire("graceful-fs"); + + // Re-require tested module + Resource = mock.reRequire("../../../../lib/lbt/resources/Resource"); + const resource = new Resource({}, "name", "file"); + await resource.buffer(); + + mock.stop("graceful-fs"); + + t.is(readFileStub.callCount, 1, "called once"); + t.is(readFileStub.getCall(0).args[0], "file", "called with file parameter"); +}); + +test.serial("Resource: constructor", async (t) => { + const resource = new Resource({}, "name", "file"); + t.is(resource.fileSize, -1, "called once"); +}); + +test.serial("Resource: constructor with stat", async (t) => { + const resource = new Resource({}, "name", "file", {size: 47}); + t.is(resource.fileSize, 47, "called once"); +}); diff --git a/test/lib/lbt/resources/ResourceCollector.js b/test/lib/lbt/resources/ResourceCollector.js new file mode 100644 index 000000000..6bd5763a6 --- /dev/null +++ b/test/lib/lbt/resources/ResourceCollector.js @@ -0,0 +1,161 @@ +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); + +let ResourceCollector = require("../../../../lib/lbt/resources/ResourceCollector"); + +test.beforeEach((t) => { + // Spying logger of processors/bootstrapHtmlTransformer + const log = require("@ui5/logger"); + const loggerInstance = log.getLogger("lbt:resources:ResourceCollector"); + mock("@ui5/logger", { + getLogger: () => loggerInstance + }); + mock.reRequire("@ui5/logger"); + t.context.logWarnSpy = sinon.spy(loggerInstance, "warn"); + + // Re-require tested module + ResourceCollector = mock.reRequire("../../../../lib/lbt/resources/ResourceCollector"); +}); + +test.afterEach.always((t) => { + mock.stop("@ui5/logger"); + t.context.logWarnSpy.restore(); +}); + + +test.serial("add: empty constructor dummy params", (t) => { + const resourceCollector = new ResourceCollector({}, {}); + t.is(resourceCollector.resources.size, 0, "empty"); +}); + +test.serial("add: empty constructor", (t) => { + const resourceCollector = new ResourceCollector(); + t.is(resourceCollector.resources.size, 0, "empty"); +}); + +test.serial("setExternalResources: empty filters", (t) => { + const resourceCollector = new ResourceCollector(); + resourceCollector.setExternalResources({ + "testcomp": [] + }); + const orphanFilters = resourceCollector.createOrphanFilters(); + t.is(orphanFilters.size, 1, "1 filter"); +}); + +test.serial("createOrphanFilters: filters", (t) => { + const resourceCollector = new ResourceCollector(); + resourceCollector.setExternalResources({ + "testcomp": ["test"], + "/": ["test"], + "": ["test"], + "a/": ["test"], + "b": ["test"], + }); + const orphanFilters = resourceCollector.createOrphanFilters(); + t.is(orphanFilters.size, 4, "4 filters"); +}); + +test.serial("visitResource: path", async (t) => { + const resourceCollector = new ResourceCollector(); + await resourceCollector.visitResource({getPath: () => "mypath", getSize: async () => 13}); + t.is(t.context.logWarnSpy.callCount, 1); + t.is(t.context.logWarnSpy.getCall(0).args[0], "non-runtime resource mypath ignored"); +}); + +test.serial("visitResource: library.source.less", async (t) => { + const resourceCollector = new ResourceCollector(); + t.is(resourceCollector.themePackages.size, 0, "initially there is no theme package"); + await resourceCollector.visitResource({getPath: () => "/resources/themes/a/library.source.less", getSize: async () => 13}); + t.is(resourceCollector.themePackages.size, 1, "theme package was added"); +}); + +test.serial("groupResourcesByComponents: debugBundles", async (t) => { + const resourceCollector = new ResourceCollector(); + resourceCollector.setExternalResources({ + "testcomp": ["my/file.js"] + }); + await resourceCollector.visitResource({getPath: () => "/resources/testcomp/Component.js", getSize: async () => 13}); + await resourceCollector.visitResource({getPath: () => "/resources/my/file.js", getSize: async () => 13}); + resourceCollector.groupResourcesByComponents({debugBundles: [".*-dbg.js"]}); + t.is(resourceCollector.resources.size, 0, "all resources were deleted"); +}); + +test.serial("groupResourcesByComponents: theme", async (t) => { + const resourceCollector = new ResourceCollector(); + await resourceCollector.visitResource({getPath: () => "/resources/themes/a/.theming", getSize: async () => 13}); + t.is(resourceCollector.themePackages.size, 1, "1 theme was added"); + await resourceCollector.determineResourceDetails({}); + resourceCollector.groupResourcesByComponents({}); + t.is(resourceCollector.themePackages.get("themes/a/").resources.length, 1, "1 theme was grouped"); +}); + +test.serial("determineResourceDetails: properties", async (t) => { + const resourceCollector = new ResourceCollector({ + getModuleInfo: async (moduleInfo) => { + return { + name: "myName" + }; + } + }); + await resourceCollector.visitResource({getPath: () => "/resources/mylib/manifest.json", getSize: async () => 13}); + await resourceCollector.visitResource({getPath: () => "/resources/mylib/i18n/i18n_de.properties", getSize: async () => 13}); + await resourceCollector.visitResource({getPath: () => "/resources/mylib/i18n/i18n.properties", getSize: async () => 13}); + await resourceCollector.determineResourceDetails({}); + resourceCollector.groupResourcesByComponents({}); + const resources = resourceCollector.components.get("mylib/").resources; + t.deepEqual(resources.map((res) => res.i18nName), [null, "i18n/i18n.properties", "i18n/i18n.properties"], "i18nName was set"); +}); + +test.serial("determineResourceDetails: view.xml", async (t) => { + const resourceCollector = new ResourceCollector({ + getModuleInfo: async (moduleInfo) => { + return { + name: "myName" + }; + } + }); + const enrichWithDependencyInfoStub = sinon.stub(resourceCollector, "enrichWithDependencyInfo").returns(Promise.resolve()); + await resourceCollector.visitResource({getPath: () => "/resources/mylib/my.view.xml", getSize: async () => 13}); + await resourceCollector.determineResourceDetails({}); + t.is(enrichWithDependencyInfoStub.callCount, 1, "is called once"); + t.is(enrichWithDependencyInfoStub.getCall(0).args[0].name, "mylib/my.view.xml", "is called with view"); +}); + +test.serial("enrichWithDependencyInfo: add infos to resourceinfo", async (t) => { + const resourceCollector = new ResourceCollector({ + getModuleInfo: async () => { + return { + name: "myname", + dynamicDependencies: true, + isConditionalDependency: (dep) => { + return dep.includes("conditional"); + }, + isImplicitDependency: (dep) => { + return dep.includes("implicit"); + }, + dependencies: [ + "mydependency.conditional", "mydependency.implicit", "mydependency" + ], + subModules: [ + "mySubmodule" + ], + requiresTopLevelScope: true, + exposedGlobals: ["myGlobal"], + rawModule: true + }; + } + }); + const resourceInfo = {}; + await resourceCollector.enrichWithDependencyInfo(resourceInfo); + t.deepEqual(resourceInfo, { + condRequired: new Set(["mydependency.conditional"]), + dynRequired: true, + exposedGlobalNames: new Set(["myGlobal"]), + format: "raw", + included: new Set(["mySubmodule"]), + module: "myname", + required: new Set(["mydependency"]), + requiresTopLevelScope: true + }, "all information gets used for the resourceInfo"); +}); diff --git a/test/lib/lbt/resources/ResourceInfo.js b/test/lib/lbt/resources/ResourceInfo.js new file mode 100644 index 000000000..67d264520 --- /dev/null +++ b/test/lib/lbt/resources/ResourceInfo.js @@ -0,0 +1,101 @@ +const test = require("ava"); + +const ResourceInfo = require("../../../../lib/lbt/resources/ResourceInfo"); + +test("ResourceInfo: constructor", async (t) => { + const resourceInfo = new ResourceInfo("myName"); + t.falsy(resourceInfo.module, "module not set"); + t.falsy(resourceInfo.format, "format not set"); + t.is(resourceInfo.size, -1, "size not set"); +}); + +test("ResourceInfo: copyFrom", async (t) => { + // setup + const resourceInfo = new ResourceInfo("myName"); + const origResourceInfo = new ResourceInfo("origMyName"); + origResourceInfo.i18nName = "i18nName"; + origResourceInfo.i18nLocale = "i18nLocale"; + origResourceInfo.isDebug = false; + origResourceInfo.theme = "theme"; + origResourceInfo.merged = false; + origResourceInfo.designtime = false; + origResourceInfo.support = false; + origResourceInfo.module = "module"; + origResourceInfo.required = new Set(["required"]); + origResourceInfo.condRequired = new Set(["condRequired"]); + origResourceInfo.included = new Set(["included"]); + origResourceInfo.dynRequired = false; + origResourceInfo.requiresTopLevelScope = false; + origResourceInfo.exposedGlobalNames = new Set(["myGlobal"]); + origResourceInfo.format = "raw"; + origResourceInfo.size = 13; + + // action + resourceInfo.copyFrom("prefix", origResourceInfo); + + // expectation + t.is(resourceInfo.i18nName, "i18nName", "value is copied over"); + t.is(resourceInfo.i18nLocale, "i18nLocale", "value is copied over"); + t.is(resourceInfo.isDebug, false, "value is copied over"); + t.is(resourceInfo.theme, "theme", "value is copied over"); + t.is(resourceInfo.merged, true, "value is copied over"); + t.is(resourceInfo.designtime, false, "value is copied over"); + t.is(resourceInfo.support, false, "value is copied over"); + t.is(resourceInfo.module, "module", "value is copied over"); + t.deepEqual(resourceInfo.required, new Set(["required"]), "value is copied over"); + t.deepEqual(resourceInfo.condRequired, new Set(["condRequired"]), "value is copied over"); + t.deepEqual(resourceInfo.included, new Set(["included"]), "value is copied over"); + t.is(resourceInfo.dynRequired, false, "value is copied over"); + t.is(resourceInfo.requiresTopLevelScope, false, "value is copied over"); + t.deepEqual(resourceInfo.exposedGlobalNames, new Set(["myGlobal"]), "value is copied over"); + t.is(resourceInfo.format, "raw", "value is copied over"); + t.is(resourceInfo.size, 13, "value is copied over"); +}); + +test("ResourceInfo: toJSON", async (t) => { + const resourceInfo = new ResourceInfo("myName"); + resourceInfo.i18nName = "i18nName"; + resourceInfo.i18nLocale = "i18nLocale"; + resourceInfo.isDebug = true; + resourceInfo.theme = "theme"; + resourceInfo.merged = true; + resourceInfo.designtime = true; + resourceInfo.support = true; + resourceInfo.module = "module"; + resourceInfo.required = new Set(["required"]); + resourceInfo.condRequired = new Set(["condRequired"]); + resourceInfo.included = new Set(["included"]); + resourceInfo.dynRequired = true; + resourceInfo.requiresTopLevelScope = true; + resourceInfo.exposedGlobalNames = new Set(["myGlobal"]); + resourceInfo.format = "raw"; + resourceInfo.size = 13; + + t.deepEqual(resourceInfo.toJSON(), { + condRequired: [ + "condRequired", + ], + designtime: true, + dynRequired: true, + exposedGlobalNames: [ + "myGlobal", + ], + format: "raw", + included: [ + "included", + ], + isDebug: true, + locale: "i18nLocale", + merged: true, + module: "module", + name: "myName", + raw: "i18nName", + required: [ + "required", + ], + requiresTopLevelScope: true, + size: 13, + support: true, + theme: "theme", + }, "json content is correct"); +}); diff --git a/test/lib/lbt/resources/ResourceInfoList.js b/test/lib/lbt/resources/ResourceInfoList.js new file mode 100644 index 000000000..8a0be81af --- /dev/null +++ b/test/lib/lbt/resources/ResourceInfoList.js @@ -0,0 +1,101 @@ +const test = require("ava"); + +const ResourceInfoList = require("../../../../lib/lbt/resources/ResourceInfoList"); + +test("add: add new resources", (t) => { + const resourceInfoList = new ResourceInfoList("prefix"); + + const myInfo = {name: "myfile.js", size: 13}; + t.deepEqual(resourceInfoList.resources, [], "empty list"); + + resourceInfoList.add(myInfo); + + t.is(resourceInfoList.resources.length, 1, "one entry"); + + const result = resourceInfoList.resources[0]; + t.falsy(result.module, "module is not set"); +}); + +test("add: add source then debug resources", (t) => { + const resourceInfoList = new ResourceInfoList("prefix"); + + resourceInfoList.add({name: "myfile.js", module: "myfile.js", size: 13}); + + const myInfo = {name: "myfile-dbg.js", size: 13}; + resourceInfoList.add(myInfo); + + t.is(resourceInfoList.resources.length, 2, "two entries"); + + const result = resourceInfoList.resourcesByName.get("../myfile.js"); + t.is(result.module, "myfile.js", "module is set"); + + const resultDbg = resourceInfoList.resourcesByName.get("../myfile-dbg.js"); + t.is(resultDbg.module, "myfile.js", "module is set"); +}); + +test("add: add debug then source resources", (t) => { + const resourceInfoList = new ResourceInfoList("prefix"); + + resourceInfoList.add({name: "myfile-dbg.js", size: 13}); + + const myInfo = {name: "myfile.js", module: "myfile.js", size: 13}; + resourceInfoList.add(myInfo); + + t.is(resourceInfoList.resources.length, 2, "two entries"); + + const result = resourceInfoList.resourcesByName.get("../myfile.js"); + t.is(result.module, "myfile.js", "module is set"); + + const resultDbg = resourceInfoList.resourcesByName.get("../myfile-dbg.js"); + t.is(resultDbg.module, "myfile.js", "module is set"); +}); + +test("add: add i18n resource", (t) => { + const resourceInfoList = new ResourceInfoList("prefix"); + + resourceInfoList.add({name: "i18n_en.properties", i18nName: "i18n.properties", size: 13}); + + t.is(resourceInfoList.resources.length, 1, "one entry"); + + const result = resourceInfoList.resourcesByName.get("../i18n_en.properties"); + t.is(result.i18nName, "../i18n.properties", "i18n name is set relative"); +}); + +test("add: resource with the same name", (t) => { + const resourceInfoList = new ResourceInfoList("prefix"); + + resourceInfoList.add({name: "myfile.js", size: 13}); + resourceInfoList.add({name: "myfile.js", size: 13}); + + t.is(resourceInfoList.resources.length, 1, "one entry"); + + const result = resourceInfoList.resourcesByName.get("../myfile.js"); + t.is(result.name, "../myfile.js", "name is set relative"); +}); + +test("toJSON: resource with the same name", (t) => { + const resourceInfoList = new ResourceInfoList("prefix"); + + resourceInfoList.resources.push({name: "myfile.js", size: 13}); + resourceInfoList.resources.push({name: "myfile.js", size: 13}); + + t.deepEqual(resourceInfoList.toJSON(), { + _version: "1.1.0", + resources: [ + { + name: "myfile.js", + size: 13, + }, + { + name: "myfile.js", + size: 13, + }, + ], + }, "one entry"); +}); + +test("makePathRelativeTo: same prefix", (t) => { + const relativePath = ResourceInfoList.makePathRelativeTo("am/bn/cf", "args/myfile.js"); + + t.is(relativePath, "../../../args/myfile.js", "relative path"); +}); diff --git a/test/lib/processors/resourceListCreator.js b/test/lib/processors/resourceListCreator.js new file mode 100644 index 000000000..27547de6c --- /dev/null +++ b/test/lib/processors/resourceListCreator.js @@ -0,0 +1,111 @@ +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); + +let resourceListCreator = require("../../../lib/processors/resourceListCreator"); +const resourceFactory = require("@ui5/fs").resourceFactory; + +test.beforeEach((t) => { + // Spying logger of processors/bootstrapHtmlTransformer + const log = require("@ui5/logger"); + const loggerInstance = log.getLogger("builder:processors:resourceListCreator"); + mock("@ui5/logger", { + getLogger: () => loggerInstance + }); + mock.reRequire("@ui5/logger"); + t.context.logErrorSpy = sinon.spy(loggerInstance, "error"); + + // Re-require tested module + resourceListCreator = mock.reRequire("../../../lib/processors/resourceListCreator"); +}); + +test.afterEach.always((t) => { + mock.stop("@ui5/logger"); + t.context.logErrorSpy.restore(); +}); + +test.serial("Empty resources", async (t) => { + const result = await resourceListCreator({ + resources: [] + }); + t.deepEqual(result, []); +}); + +test.serial("Empty resources but options", async (t) => { + const result = await resourceListCreator({ + resources: [] + }, { + externalResources: { + "mycomp": [".*dbg.js"] + } + }); + t.deepEqual(result, []); +}); + +test.serial("Orphaned resources", async (t) => { + const resource = resourceFactory.createResource({ + path: "/resources/nomodule.foo", + string: "bar content" + }); + const errorObject = await t.throwsAsync(() => { + return resourceListCreator({ + resources: [resource] + }); + }); + t.is(errorObject.message, "resources.json generation failed with error: There are 1 resources which could not be assigned to components."); + t.is(t.context.logErrorSpy.callCount, 1); + t.is(t.context.logErrorSpy.getCall(0).args[0], "resources.json generation failed because of unassigned resources: nomodule.foo"); +}); + +// 114,134-168,174-175 + +test.serial("components and themes", async (t) => { + const componentResource = resourceFactory.createResource({ + path: "/resources/mylib/manifest.json", + string: "bar content" + }); + const themeResource = resourceFactory.createResource({ + path: "/resources/themes/a/.theming", + string: "base less content" + }); + const resources = await resourceListCreator({ + resources: [componentResource, themeResource] + }); + + t.is(resources.length, 2); + const libResourceJson = resources[0]; + const themeResourceJson = resources[1]; + t.is(libResourceJson.getPath(), "/resources/mylib/resources.json"); + t.is(themeResourceJson.getPath(), "/resources/themes/a/resources.json"); + + const libResourcesJsonContent = await libResourceJson.getString(); + const themeResourcesJsonContent = await themeResourceJson.getString(); + t.is(libResourcesJsonContent, `{ + "_version": "1.1.0", + "resources": [ + { + "name": "manifest.json", + "module": "mylib/manifest.json", + "size": 11 + }, + { + "name": "resources.json", + "size": 183 + } + ] +}`); + t.is(themeResourcesJsonContent, `{ + "_version": "1.1.0", + "resources": [ + { + "name": ".theming", + "size": 17, + "theme": "a" + }, + { + "name": "resources.json", + "size": 159 + } + ] +}`); +}); diff --git a/test/lib/tasks/generateResourcesJson.js b/test/lib/tasks/generateResourcesJson.js new file mode 100644 index 000000000..992f20bc3 --- /dev/null +++ b/test/lib/tasks/generateResourcesJson.js @@ -0,0 +1,68 @@ +const test = require("ava"); +const sinon = require("sinon"); +const mock = require("mock-require"); + + +const ui5Fs = require("@ui5/fs"); +const resourceFactory = ui5Fs.resourceFactory; + +function createWorkspace() { + return resourceFactory.createAdapter({ + virBasePath: "/", + project: { + metadata: { + name: "test.lib" + }, + version: "2.0.0", + dependencies: [ + { + metadata: { + name: "sap.ui.core" + }, + version: "1.0.0" + } + ] + } + }); +} + +test.beforeEach((t) => { + t.context.resourceListCreatorStub = sinon.stub(); + t.context.resourceListCreatorStub.returns(Promise.resolve([])); + mock("../../../lib/processors/resourceListCreator", t.context.resourceListCreatorStub); + mock.reRequire("../../../lib/processors/resourceListCreator"); +}); + +test.afterEach.always((t) => { + mock.stop("../../../lib/processors/resourceListCreator"); +}); + +test("empty resources", async (t) => { + const generateResourcesJson = require("../../../lib/tasks/generateResourcesJson"); + + const result = await generateResourcesJson({workspace: createWorkspace(), dependencies: undefined, options: {projectName: "sap.ui.core"}}); + t.deepEqual(result, undefined, "no resources returned"); + t.is(t.context.resourceListCreatorStub.callCount, 1); + const expectedOptions = { + externalResources: { + "sap/ui/core": [ + "*", + "sap/base/", + "sap/ui/" + ] + }, + mergedResourcesFilters: [ + "jquery-sap*.js", + "sap-ui-core*.js", + "**/Component-preload.js", + "**/library-preload.js", + "**/library-preload-dbg.js", + "**/library-preload.json", + "**/library-all.js", + "**/library-all-dbg.js", + "**/designtime/library-preload.designtime.js", + "**/library-preload.support.js" + ] + }; + t.deepEqual(t.context.resourceListCreatorStub.getCall(0).args[1], expectedOptions, "options match"); +}); diff --git a/test/lib/tasks/uglify.js b/test/lib/tasks/uglify.js index fe397df7d..135743494 100644 --- a/test/lib/tasks/uglify.js +++ b/test/lib/tasks/uglify.js @@ -47,3 +47,142 @@ test();`; return t.deepEqual(buffer.toString(), expected, "Correct content"); }); }); + +test("integration: uglify copyright", (t) => { + const reader = resourceFactory.createAdapter({ + virBasePath: "/" + }); + const writer = resourceFactory.createAdapter({ + virBasePath: "/" + }); + const duplexCollection = new DuplexCollection({reader: reader, writer: writer}); + const content = ` +/* + * Copyright jQuery Foundation and other contributors + */ +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test();`; + const testResource = resourceFactory.createResource({ + path: "/test.js", + string: content + }); + const expected = `/* + * Copyright jQuery Foundation and other contributors + */ +function test(t){var o=t;console.log(o)}test();`; + + return reader.write(testResource) + .then(() => { + return reader.byPath("/test.js"); + }).then(() => { + return uglify({ + workspace: duplexCollection, + options: { + pattern: "/test.js" + } + }); + }).then(() => { + return writer.byPath("/test.js").then((resource) => { + if (!resource) { + t.fail("Could not find /test.js in target locator"); + } else { + return resource.getBuffer(); + } + }); + }).then((buffer) => { + return t.deepEqual(buffer.toString(), expected, "Correct content"); + }); +}); + +test("integration: uglify raw module (@ui5-bundle-raw-include)", (t) => { + const reader = resourceFactory.createAdapter({ + virBasePath: "/" + }); + const writer = resourceFactory.createAdapter({ + virBasePath: "/" + }); + const duplexCollection = new DuplexCollection({reader: reader, writer: writer}); + const content = ` +//@ui5-bundle-raw-include sap/ui/my/module.js +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test();`; + const testResource = resourceFactory.createResource({ + path: "/test.js", + string: content + }); + const expected = `//@ui5-bundle-raw-include sap/ui/my/module.js +function test(t){var o=t;console.log(o)}test();`; + + return reader.write(testResource) + .then(() => { + return reader.byPath("/test.js"); + }).then(() => { + return uglify({ + workspace: duplexCollection, + options: { + pattern: "/test.js" + } + }); + }).then(() => { + return writer.byPath("/test.js").then((resource) => { + if (!resource) { + t.fail("Could not find /test.js in target locator"); + } else { + return resource.getBuffer(); + } + }); + }).then((buffer) => { + return t.deepEqual(buffer.toString(), expected, "Correct content"); + }); +}); + +test("integration: uglify raw module (@ui5-bundle)", (t) => { + const reader = resourceFactory.createAdapter({ + virBasePath: "/" + }); + const writer = resourceFactory.createAdapter({ + virBasePath: "/" + }); + const duplexCollection = new DuplexCollection({reader: reader, writer: writer}); + const content = ` +//@ui5-bundle sap/ui/my/module.js +function test(paramA) { + var variableA = paramA; + console.log(variableA); +} +test();`; + const testResource = resourceFactory.createResource({ + path: "/test.js", + string: content + }); + const expected = `//@ui5-bundle sap/ui/my/module.js +function test(t){var o=t;console.log(o)}test();`; + + return reader.write(testResource) + .then(() => { + return reader.byPath("/test.js"); + }).then(() => { + return uglify({ + workspace: duplexCollection, + options: { + pattern: "/test.js" + } + }); + }).then(() => { + return writer.byPath("/test.js").then((resource) => { + if (!resource) { + t.fail("Could not find /test.js in target locator"); + } else { + return resource.getBuffer(); + } + }); + }).then((buffer) => { + return t.deepEqual(buffer.toString(), expected, "Correct content"); + }); +}); diff --git a/test/lib/types/application/ApplicationBuilder.js b/test/lib/types/application/ApplicationBuilder.js index f05d56552..b37cb222f 100644 --- a/test/lib/types/application/ApplicationBuilder.js +++ b/test/lib/types/application/ApplicationBuilder.js @@ -70,7 +70,8 @@ test("Instantiation", (t) => { "uglify", "generateVersionInfo", "generateCachebusterInfo", - "generateApiIndex" + "generateApiIndex", + "generateResourcesJson" ], "ApplicationBuilder is instantiated with standard tasks"); }); @@ -93,7 +94,8 @@ test("Instantiation without component preload project configuration", (t) => { "uglify", "generateVersionInfo", "generateCachebusterInfo", - "generateApiIndex" + "generateApiIndex", + "generateResourcesJson" ], "ApplicationBuilder is still instantiated with standard tasks"); }); @@ -114,7 +116,8 @@ test("Instantiation without project namespace", (t) => { "createDebugFiles", "uglify", "generateVersionInfo", - "generateApiIndex" + "generateApiIndex", + "generateResourcesJson" ], "All standard tasks but generateComponentPreload will be executed"); }); @@ -142,6 +145,7 @@ test("Instantiation with custom tasks", (t) => { "replaceVersion--1", "generateVersionInfo", "generateCachebusterInfo", - "generateApiIndex" + "generateApiIndex", + "generateResourcesJson" ], "ApplicationBuilder is still instantiated with standard tasks"); });