UNPKG

@ui5/builder

Version:
354 lines (320 loc) 10.9 kB
import {pd} from "pretty-data"; import {toRequireJSName} from "../utils/ModuleName.js"; import {SectionType} from "./BundleDefinition.js"; import escapePropertiesFile from "../utils/escapePropertiesFile.js"; import {makeStringLiteral} from "../utils/stringUtils.js"; import {getLogger} from "@ui5/logger"; const log = getLogger("lbt:bundle:AutoSplitter"); const xmlHtmlPrePattern = /<(?:\w+:)?pre\b/; /** * * @author Frank Weigel * @since 1.27.1 * @private */ class AutoSplitter { /** * Used to split resources * * @param {ResourcePool} pool * @param {Resolver} resolver */ constructor(pool, resolver) { this.pool = pool; this.resolver = resolver; this.optimizeXMLViews = false; } // TODO refactor JSMergedModuleBuilder(Ext) so that it can be used with a NullWriter to collect all resource sizes // this would avoid the redundant compression code in this class /** * Runs the split operation * * @param {object} moduleDef * @param {object} options * @returns {Promise<Array>} */ async run(moduleDef, options) { options = options || {}; const numberOfParts = options.numberOfParts; let totalSize = 0; const moduleSizes = Object.create(null); const depCacheSizes = []; let depCacheLoaderSize = 0; let bundleInfoLoaderSize = 0; this.optimize = !!options.optimize; // ---- resolve module definition const resolvedModule = await this.resolver.resolve(moduleDef); // ---- calculate overall size of merged module if ( moduleDef.configuration ) { totalSize += 1024; // just a rough estimate } const promises = []; resolvedModule.sections.forEach( (section) => { switch ( section.mode ) { case SectionType.Provided: // provided modules don't contribute to the final size break; case SectionType.Raw: case SectionType.Preload: section.modules.forEach( (module) => { promises.push( this._calcMinSize(module).then( (size) => { totalSize += size; moduleSizes[module] = size; }) ); }); break; case SectionType.Require: section.modules.forEach( (module) => { totalSize += "sap.ui.requireSync('');".length + toRequireJSName(module).length; }); break; case SectionType.DepCache: depCacheLoaderSize = "sap.ui.loader.config({depCacheUI5:{}});".length; totalSize += depCacheLoaderSize; section.modules.forEach( (module) => { promises.push((async () => { const resource = await this.pool.findResourceWithInfo(module); const deps = resource.info.dependencies.filter( (dep) => !resource.info.isConditionalDependency(dep) && !resource.info.isImplicitDependency(dep) ); if (deps.length > 0) { const depSize = `"${module}": [${deps.map((dep) => `"${dep}"`).join(",")}],`.length; totalSize += depSize; depCacheSizes.push({size: depSize, module}); } })()); }); break; case SectionType.BundleInfo: bundleInfoLoaderSize = "sap.ui.loader.config({bundlesUI5:{\n\n}});\n".length; totalSize += bundleInfoLoaderSize; totalSize += `"${section.name}":[${section.modules.map(makeStringLiteral).join(",")}]`.length; break; default: throw new Error(`Unknown section mode: ${section.mode}`); } }); await Promise.all(promises); const partSize = Math.floor(totalSize / numberOfParts); log.verbose( `Total size of modules ${totalSize} (chars), target size for each ` + `of the ${numberOfParts} parts: ${partSize} (chars)`); // ---- create a separate module definition for each part const splittedModules = []; let moduleNameWithPart = moduleDef.name; if ( !/__part__/.test(moduleNameWithPart) ) { moduleNameWithPart = toRequireJSName(moduleNameWithPart) + "-__part__.js"; } // vars = Object.create(null); let part = 0; totalSize = 0; let currentModule = { name: moduleNameWithPart.replace(/__part__/, part), sections: [] }; // vars.put("part", Integer.toString(part)); // currentModule.setName((ModuleName) ModuleNamePattern.resolvePlaceholders(moduleNameWithPart, vars)); splittedModules.push(currentModule); if ( moduleDef.configuration ) { currentModule.configuration = moduleDef.configuration; totalSize += 1024; // TODO calculate a real size? } resolvedModule.sections.forEach( (section) => { let currentSection; let sequence; let bundleInfoModuleStringLiterals; switch ( section.mode ) { case SectionType.Provided: // 'provided' sections are no longer needed in a fully resolved module break; case SectionType.Raw: // raw sections are always copied as a whole currentSection = { mode: SectionType.Raw, filters: [] }; currentSection.declareRawModules = section.sectionDefinition.declareRawModules; currentSection.sort = section.sectionDefinition.sort; currentModule.sections.push( currentSection ); section.modules.forEach( (module) => { currentSection.filters.push(module); totalSize += moduleSizes[module]; }); break; case SectionType.Preload: sequence = section.modules.slice(); // simple version: just sort alphabetically sequence.sort(); // NODE_TODO: sort by copyright: // jsBuilder.beforeWriteFunctionPreloadSection((List<ModuleName>) sequence); currentSection = { mode: SectionType.Preload, filters: [] }; currentSection.name = section.name; currentModule.sections.push( currentSection ); sequence.forEach( (module) => { const moduleSize = moduleSizes[module]; if ( part + 1 < numberOfParts && totalSize + moduleSize / 2 > partSize ) { part++; // start a new module totalSize = 0; currentModule = { name: moduleNameWithPart.replace(/__part__/, part), sections: [] }; // vars.put("part", Integer.toString(part)); // currentModule.setName( // (ModuleName) ModuleNamePattern.resolvePlaceholders(moduleNameWithPart, vars)); splittedModules.push(currentModule); currentSection = { name: section.name, mode: SectionType.Preload, filters: [] }; currentModule.sections.push( currentSection ); } // add module to current section currentSection.filters.push(module); totalSize += moduleSize; }); break; case SectionType.Require: currentSection = { mode: SectionType.Require, filters: [] }; currentModule.sections.push( currentSection ); section.modules.forEach( (module) => { currentSection.filters.push( module ); totalSize += 21 + toRequireJSName(module).length; }); break; case SectionType.DepCache: currentSection = { mode: SectionType.DepCache, filters: [] }; currentModule.sections.push( currentSection ); totalSize += depCacheLoaderSize; depCacheSizes.forEach((depCache) => { if ( part + 1 < numberOfParts && totalSize + depCache.size / 2 > partSize ) { part++; // start a new module totalSize = depCacheLoaderSize; currentSection = { mode: SectionType.DepCache, filters: [] }; currentModule = { name: moduleNameWithPart.replace(/__part__/, part), sections: [currentSection] }; splittedModules.push(currentModule); } if (!currentSection.filters.includes(depCache.module)) { currentSection.filters.push(depCache.module); totalSize += depCache.size; } }); break; case SectionType.BundleInfo: // Create new part if size is already exceeded if (part + 1 < numberOfParts && totalSize > partSize) { part++; currentModule = { name: moduleNameWithPart.replace(/__part__/, part), sections: [] }; splittedModules.push(currentModule); } // bundleInfo sections are always copied as a whole currentSection = { mode: SectionType.BundleInfo, name: section.name, filters: [] }; currentModule.sections.push(currentSection); bundleInfoModuleStringLiterals = []; section.modules.forEach((module) => { currentSection.filters.push(module); bundleInfoModuleStringLiterals.push(makeStringLiteral(module)); }); totalSize += bundleInfoLoaderSize; totalSize += `"${section.name}":[${bundleInfoModuleStringLiterals.join(",")}]`.length; break; default: throw new Error(`Unknown section mode: ${section.mode}`); } }); log.verbose(`Splitted modules: ${splittedModules}`); return splittedModules; } async _calcMinSize(module) { const resource = await this.pool.findResourceWithInfo(module); if ( resource != null ) { if ( resource.info && resource.info.compressedSize && resource.info.compressedSize !== resource.info.size ) { return resource.info.compressedSize; } if ( /\.js$/.test(module) ) { // No optimize / minify step here as the input should be // either already optimized or not, based on the bundle options const fileContent = await resource.buffer(); return fileContent.length; } else if ( /\.properties$/.test(module) ) { /* NODE-TODO minimize *.properties Properties props = new Properties(); props.load(in); in.close(); Writer out = new StringWriter(); props.store(out, ""); return out.toString().length(); */ // Since AutoSplitter is also used when splitting non-project resources (e.g. dependencies) // *.properties files should be escaped if encoding option is specified const fileContent = await escapePropertiesFile(resource); return fileContent.length; } else if ( this.optimizeXMLViews && /\.view.xml$/.test(module) ) { // needs to be activated when it gets activated in JSMergedModuleBuilderExt let fileContent = await resource.buffer(); if ( this.optimize ) { // For XML we use the pretty data // Do not minify if XML(View) contains an <*:pre> tag because whitespace of // HTML <pre> should be preserved (should only happen rarely) if (!xmlHtmlPrePattern.test(fileContent.toString())) { fileContent = pd.xmlmin(fileContent.toString(), false); } } return fileContent.length; } // if there is no precompiled information about the resource, just determine its length if ( !resource.info && /\.(js|json|xml)$/.test(module) ) { const fileContent = await resource.buffer(); return fileContent.length; } } return resource && resource.info && resource.info.size ? resource.info.size : 0; } /* NODE-TODO debug resources private URL findResource(ModuleName name) { URL result = null; if ( debugMode ) { ModuleName debugName = ModuleName.getDebugName(name); if ( debugName != null ) { result = pool.findResource(debugName); } } if ( result == null ) { result = pool.findResource(name); } return result; } */ } export default AutoSplitter;