diff --git a/Gruntfile.js b/Gruntfile.js index ca2685f6b5..fea952d31b 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -44,11 +44,17 @@ module.exports = grunt => { const originalLibDir = 'client/lib/original'; - let bundleFileMap = { - 'client/lib/espo-libs.min.js': buildUtils.getPreparedBundleLibList(libs), - 'client/lib/espo-templates.min.js': 'client/lib/original/espo-templates.js', - 'client/lib/espo.min.js': originalLibDir + `/espo.js`, - }; + let bundleFileMap = {'client/lib/espo-libs.min.js': buildUtils.getPreparedBundleLibList(libs)}; + + for (let name in bundleConfig.chunks) { + let namePart = 'espo-' + name; + + if (name === 'main') { + namePart = 'espo'; + } + + bundleFileMap[`client/lib/${namePart}.min.js`] = originalLibDir + `/${namePart}.js` + } let copyJsFileList = buildUtils.getCopyLibDataList(libs); @@ -255,43 +261,34 @@ module.exports = grunt => { }, }); - grunt.registerTask('bundle-espo', () => { - let chunkConfig = bundleConfig.chunks.main; - - let contents = (new BundlerGeneral()).bundle({ - files: chunkConfig.files, - patterns: chunkConfig.patterns, - allPatterns: ['client/src/**/*.js'], - libs: libs, - }); - - contents += '\n' + - (new LayoutTypeBundler()).bundle(); - - let file = originalLibDir + `/espo.js`; - + const writeOriginalLib = (name, contents) => { if (!fs.existsSync(originalLibDir)) { fs.mkdirSync(originalLibDir); } + let file = originalLibDir + `/${name}.js`; + fs.writeFileSync(file, contents, 'utf8'); - }); + }; - grunt.registerTask('bundle-templates', () => { - let chunkConfig = bundleConfig.chunks.templates; + grunt.registerTask('bundle', () => { + let bundler = new BundlerGeneral(bundleConfig, libs); - let contents = (new BundlerGeneral()).bundle({ - templatePatterns: chunkConfig.templatePatterns, - modulePaths: {'crm': 'client/modules/crm'}, - }); + let result = bundler.bundle(); - let file = originalLibDir + `/espo-templates.js`; + for (let name in result) { + let contents = result[name]; - if (!fs.existsSync(originalLibDir)) { - fs.mkdirSync(originalLibDir); + let key = 'espo-' + name; + + if (name === 'main') { + contents += '\n' + (new LayoutTypeBundler()).bundle(); + + key = 'espo'; + } + + writeOriginalLib(key, contents); } - - fs.writeFileSync(file, contents, 'utf8'); }); grunt.registerTask('prepare-lib-original', () => { @@ -476,9 +473,8 @@ module.exports = grunt => { grunt.registerTask('internal', [ 'less', 'cssmin', - 'bundle-espo', - 'bundle-templates', 'prepare-lib-original', + 'bundle', 'uglify:bundle', 'copy:frontendLib', 'prepare-lib', diff --git a/client/res/templates/fields/barcode/detail.tpl b/client/res/templates/fields/barcode/detail.tpl index e3ebf6f790..312e1df956 100644 --- a/client/res/templates/fields/barcode/detail.tpl +++ b/client/res/templates/fields/barcode/detail.tpl @@ -1,6 +1,6 @@ {{#if isNotEmpty}} -{{#if viewObject.isSvg}} +{{#if isSvg}} {{else}}
diff --git a/client/src/loader.js b/client/src/loader.js index 38f9dd75d3..79628d7b63 100644 --- a/client/src/loader.js +++ b/client/src/loader.js @@ -74,6 +74,10 @@ this._cacheIsSet = false; this._responseCacheIsSet = false; this._internalModuleListIsSet = false; + this._bundleFileMap = {}; + this._bundleMapping = {}; + /** @type Object. */ + this._bundleDependenciesMap = {}; this._addLibsConfigCallCount = 0; this._addLibsConfigCallMaxCount = 2; @@ -500,6 +504,25 @@ return; } + if (name in this._bundleMapping) { + let bundleName = this._bundleMapping[name]; + + this._addBundle(bundleName).then(() => { + let classObj = this._getClass(name); + + if (!classObj) { + let msg = `Could not obtain class '${name}' from bundle '${bundleName}'.`; + console.error(msg); + + throw new Error(msg); + } + + callback(classObj); + }); + + return; + } + path = this._nameToPath(name); } @@ -578,6 +601,65 @@ }); }, + /** + * @private + * @param {string} name + * @return {Promise} + */ + _addBundle: function (name) { + let dependencies = this._bundleDependenciesMap[name] || []; + + if (!dependencies.length) { + return this._addBundleInternal(name); + } + + return new Promise(resolve => { + let list = dependencies.map(item => Espo.loader.requirePromise(item)); + + Promise.all(list) + .then(() => { + return this._addBundleInternal(name); + }) + .then(() => resolve()); + }); + }, + + /** + * @private + * @param {string} name + * @return {Promise} + */ + _addBundleInternal: function (name) { + let src = this._bundleFileMap[name]; + + if (!src) { + throw new Error(`Unknown bundle '${name}'.`); + } + + if (this._cacheTimestamp) { + let sep = (src.indexOf('?') > -1) ? '&' : '?'; + + src += sep + 'r=' + this._cacheTimestamp; + } + + src = this._basePath + src; + + let scriptEl = document.createElement('script'); + + scriptEl.setAttribute('type', 'text/javascript') + scriptEl.setAttribute('src', src); + + scriptEl.addEventListener('error', event => { + console.error(`Could not load bundle '${name}'.`, event); + }); + + return new Promise(resolve => { + document.head.appendChild(scriptEl); + + scriptEl.addEventListener('load', () => resolve()); + }); + }, + /** * @private */ @@ -756,6 +838,32 @@ return this._internalModuleMap[moduleName]; }, + /** + * @param {string} name A bundle name. + * @param {string} file A bundle file. + * @internal + */ + mapBundleFile: function (name, file) { + this._bundleFileMap[name] = file; + }, + + /** + * @param {string} name A bundle name. + * @param {string} list Dependencies.. + * @internal + */ + mapBundleDependencies: function (name, list) { + this._bundleDependenciesMap[name] = list; + }, + + /** + * @param {Object.} mapping + * @internal + */ + addBundleMapping: function (mapping) { + Object.assign(this._bundleMapping, mapping); + }, + /** * Require a module or multiple modules. * @@ -871,6 +979,32 @@ addLibsConfig: function (data) { loader.addLibsConfig(data); }, + + /** + * @param {string} name A bundle name. + * @param {string} file A bundle file. + * @internal + */ + mapBundleFile: function (name, file) { + loader.mapBundleFile(name, file); + }, + + /** + * @param {string} name A bundle name. + * @param {string} list Dependencies.. + * @internal + */ + mapBundleDependencies: function (name, list) { + loader.mapBundleDependencies(name, list); + }, + + /** + * @param {Object.} mapping + * @internal + */ + addBundleMapping: function (mapping) { + loader.addBundleMapping(mapping); + }, }; /** diff --git a/client/src/views/fields/barcode.js b/client/src/views/fields/barcode.js index 0b51faec24..74f0898255 100644 --- a/client/src/views/fields/barcode.js +++ b/client/src/views/fields/barcode.js @@ -26,16 +26,16 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -define('views/fields/barcode', - ['views/fields/varchar', 'lib!JsBarcode', 'lib!qrcode'], -function (Dep, JsBarcode, QRCode) { +define('views/fields/barcode', ['views/fields/varchar'], function (Dep) { + + let JsBarcode; + let QRCode; return Dep.extend({ type: 'barcode', listTemplate: 'fields/barcode/detail', - detailTemplate: 'fields/barcode/detail', setup: function () { @@ -66,21 +66,44 @@ function (Dep, JsBarcode, QRCode) { if (this.params.codeType !== 'QRcode') { this.isSvg = true; + + this.wait( + Espo.loader.requirePromise('lib!JsBarcode').then(lib => { + JsBarcode = lib; + + console.log(JsBarcode); + }) + ); + } + else { + this.wait( + Espo.loader.requirePromise('lib!qrcode').then(lib => { + QRCode = lib; + }) + ); } Dep.prototype.setup.call(this); - $(window).on('resize.' + this.cid, function () { + $(window).on('resize.' + this.cid, () => { if (!this.isRendered()) { return; } this.controlWidth(); - }.bind(this)); + }); this.listenTo(this.recordHelper, 'panel-show', () => this.controlWidth()); }, + data: function () { + let data = Dep.prototype.data.call(this); + + data.isSvg = this.isSvg; + + return data; + }, + onRemove: function () { $(window).off('resize.' + this.cid); }, @@ -122,15 +145,10 @@ function (Dep, JsBarcode, QRCode) { } else { // SVG may be not available yet (in webkit). - setTimeout( - function () { - this.initBarcode(value); - - this.controlWidth(); - } - .bind(this), - 100 - ); + setTimeout(() => { + this.initBarcode(value); + this.controlWidth(); + }, 100); } } diff --git a/client/src/views/fields/colorpicker.js b/client/src/views/fields/colorpicker.js index ee29e22411..1e7351b868 100644 --- a/client/src/views/fields/colorpicker.js +++ b/client/src/views/fields/colorpicker.js @@ -26,20 +26,20 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -define('views/fields/colorpicker', ['views/fields/varchar', 'lib!Colorpicker'], function (Dep, Colorpicker) { +define('views/fields/colorpicker', ['views/fields/varchar'], function (Dep) { return Dep.extend({ type: 'varchar', detailTemplate: 'fields/colorpicker/detail', - listTemplate: 'fields/colorpicker/detail', - editTemplate: 'fields/colorpicker/edit', setup: function () { Dep.prototype.setup.call(this); + + this.wait(Espo.loader.requirePromise('lib!Colorpicker')); }, afterRender: function () { diff --git a/client/src/views/modals/image-crop.js b/client/src/views/modals/image-crop.js index 88087b90bd..4b1ac7c23f 100644 --- a/client/src/views/modals/image-crop.js +++ b/client/src/views/modals/image-crop.js @@ -26,7 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -define('views/modals/image-crop', ['views/modal', 'lib!Cropper'], function (Dep, Cropper) { +define('views/modals/image-crop', ['views/modal'], function (Dep) { return Dep.extend({ @@ -40,7 +40,7 @@ define('views/modals/image-crop', ['views/modal', 'lib!Cropper'], function (Dep, }, 'click [data-action="zoomOut"]': function () { this.$img.cropper('zoom', -0.1); - } + }, }, setup: function () { @@ -56,6 +56,8 @@ define('views/modals/image-crop', ['views/modal', 'lib!Cropper'], function (Dep, }, ]; + this.wait(Espo.loader.requirePromise('lib!Cropper')); + this.on('remove', () => { if (this.$img.length) { this.$img.cropper('destroy'); @@ -65,13 +67,13 @@ define('views/modals/image-crop', ['views/modal', 'lib!Cropper'], function (Dep, }, afterRender: function () { - var $img = this.$img = $('') + let $img = this.$img = $('') .attr('src', this.options.contents) .addClass('hidden'); this.$el.find('.image-container').append($img); - setTimeout(function () { + setTimeout(() => { $img.cropper({ aspectRatio: 1, movable: true, diff --git a/client/src/views/modals/image-preview.js b/client/src/views/modals/image-preview.js index e5aa77f895..1cbda249a4 100644 --- a/client/src/views/modals/image-preview.js +++ b/client/src/views/modals/image-preview.js @@ -26,7 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -define('views/modals/image-preview', ['views/modal', 'lib!exif'], function (Dep) { +define('views/modals/image-preview', ['views/modal'], function (Dep) { return Dep.extend({ @@ -82,6 +82,8 @@ define('views/modals/image-preview', ['views/modal', 'lib!exif'], function (Dep) this.once('remove', () => { $(window).off('resize.image-review'); }); + + this.wait(Espo.loader.requirePromise('lib!exif')); }, getImageUrl: function () { diff --git a/client/src/views/scheduled-job/fields/scheduling.js b/client/src/views/scheduled-job/fields/scheduling.js index 9fbcaba067..a38559f3da 100644 --- a/client/src/views/scheduled-job/fields/scheduling.js +++ b/client/src/views/scheduled-job/fields/scheduling.js @@ -26,8 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -define('views/scheduled-job/fields/scheduling', -['views/fields/varchar', 'lib!cronstrue'], function (Dep, cronstrue) { +define('views/scheduled-job/fields/scheduling', ['views/fields/varchar'], function (Dep) { return Dep.extend({ @@ -35,9 +34,14 @@ define('views/scheduled-job/fields/scheduling', Dep.prototype.setup.call(this); if (this.isEditMode() || this.isDetailMode()) { - this.listenTo(this.model, 'change:' + this.name, function () { - this.showText(); - }, this); + this.wait( + Espo.loader.requirePromise('lib!cronstrue') + .then(Cronstrue => { + this.Cronstrue = Cronstrue; + + this.listenTo(this.model, 'change:' + this.name, () => this.showText()); + }) + ); } }, @@ -45,7 +49,8 @@ define('views/scheduled-job/fields/scheduling', Dep.prototype.afterRender.call(this); if (this.isEditMode() || this.isDetailMode()) { - var $text = this.$text = $('
'); + let $text = this.$text = $('
'); + this.$el.append($text); this.showText(); } @@ -56,6 +61,10 @@ define('views/scheduled-job/fields/scheduling', return; } + if (!this.Cronstrue) { + return; + } + var exp = this.model.get(this.name); if (!exp) { @@ -71,7 +80,7 @@ define('views/scheduled-job/fields/scheduling', } var locale = 'en'; - var localeList = Object.keys(cronstrue.default.locales); + var localeList = Object.keys(this.Cronstrue.default.locales); var language = this.getLanguage().name; if (~localeList.indexOf(language)) { @@ -82,7 +91,7 @@ define('views/scheduled-job/fields/scheduling', } try { - var text = cronstrue.toString(exp, { + var text = this.Cronstrue.toString(exp, { use24HourTimeFormat: !this.getDateTime().hasMeridian(), locale: locale, }); diff --git a/client/src/views/user-security/modals/totp.js b/client/src/views/user-security/modals/totp.js index caa85e9546..19e308dd02 100644 --- a/client/src/views/user-security/modals/totp.js +++ b/client/src/views/user-security/modals/totp.js @@ -26,9 +26,9 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -define('views/user-security/modals/totp', - ['views/modal', 'model', 'lib!qrcode'], - function (Dep, Model, QRCode) { +define('views/user-security/modals/totp', ['views/modal', 'model'], function (Dep, Model) { + + let QRCode; return Dep.extend({ @@ -114,6 +114,10 @@ define('views/user-security/modals/totp', } ], }); + + Espo.loader.requirePromise('lib!qrcode').then(lib => { + QRCode = lib; + }) }, afterRender: function () { diff --git a/frontend/bundle-config.json b/frontend/bundle-config.json index e9e9f9a8a8..c1cb2934da 100644 --- a/frontend/bundle-config.json +++ b/frontend/bundle-config.json @@ -1,4 +1,15 @@ { + "order": [ + "main", + "templates", + "admin", + "crm", + "wysiwyg", + "chart", + "calendar", + "timeline", + "extra" + ], "chunks": { "main": { "files": [ @@ -65,6 +76,105 @@ "client/res/templates/fields/varchar/*.tpl", "client/res/templates/fields/wysiwyg/*.tpl" ] + }, + "admin": { + "patterns": [ + "client/src/views/admin/**/*.js", + "client/src/views/authentication-provider/**/*.js", + "client/src/views/inbound-email/**/*.js", + "client/src/views/templates/**/*.js", + "client/src/views/role/**/*.js", + "client/src/views/portal-role/**/*.js", + "client/src/views/scheduled-job/**/*.js", + "client/src/views/lead-capture/**/*.js", + "client/src/views/lead-capture-log-record/**/*.js", + "client/src/views/webhooks/**/*.js", + "client/src/controllers/admin.js", + "client/src/controllers/inbound-email.js", + "client/src/controllers/layout-set.js", + "client/src/controllers/api-user.js", + "client/src/controllers/role.js", + "client/src/controllers/portal-role.js" + ], + "templatePatterns": [ + "client/res/templates/admin/**/*.tpl" + ] + }, + "crm": { + "patterns": [ + "client/modules/crm/src/**/*.js" + ], + "templatePatterns": [ + "client/modules/crm/res/templates/**/*.tpl" + ], + "allPatterns": [ + "client/modules/crm/src/**/*.js" + ] + }, + "wysiwyg": { + "patterns": [ + "client/src/**/*.js", + "client/modules/crm/src/**/*.js" + ], + "dependentOn": [ + "lib!Summernote" + ], + "libs": ["Summernote"], + "allPatterns": [ + "client/modules/crm/src/**/*.js" + ] + }, + "chart": { + "patterns": [ + "client/modules/crm/src/**/*.js" + ], + "dependentOn": [ + "lib!Flotr", + "lib!espo-funnel-chart" + ], + "libs": ["Flotr", "espo-funnel-chart"], + "allPatterns": [ + "client/modules/crm/src/**/*.js" + ] + }, + "calendar": { + "patterns": [ + "client/modules/crm/src/**/*.js" + ], + "dependentOn": [ + "lib!full-calendar" + ], + "libs": ["full-calendar"], + "allPatterns": [ + "client/modules/crm/src/**/*.js" + ] + }, + "timeline": { + "patterns": [ + "client/modules/crm/src/**/*.js" + ], + "dependentOn": [ + "lib!vis" + ], + "libs": ["vis"], + "allPatterns": [ + "client/modules/crm/src/**/*.js" + ] + }, + "extra": { + "patterns": [ + "client/src/**/*.js" + ], + "templatePatterns": [ + "client/res/templates/**/*.tpl" + ], + "noDuplicates": true } - } + }, + "modulePaths": { + "crm": "client/modules/crm" + }, + "allPatterns": [ + "client/src/**/*.js" + ] } diff --git a/js/build-utils.js b/js/build-utils.js index 226e4a6043..d53364993a 100644 --- a/js/build-utils.js +++ b/js/build-utils.js @@ -71,8 +71,8 @@ let BuildUtils = { * src: string, * dest: string, * originalDest: string|null, - * minify: boolean - * }[]} + * minify: boolean, + * }[]} */ getCopyLibDataList: function (libs) { let list = []; diff --git a/js/bundler-general.js b/js/bundler-general.js index 7881f1664b..4b98f4084b 100644 --- a/js/bundler-general.js +++ b/js/bundler-general.js @@ -28,47 +28,222 @@ const Bundler = require("./bundler"); const Precompiler = require('./template-precompiler'); +const fs = require('fs'); class BundlerGeneral { /** * @param {{ - * files: string[], - * patterns?: string[], - * allPatterns?: string[], - * libs: { - * src?: string, - * bundle?: boolean, - * key?: string, - * }[], - * templatePatterns?: string[], - * modulePaths?: Object., - * }} params - * @return {string} + * chunks: Object., + * modulePaths?: Record., + * allPatterns: string[], + * order: string[], + * }} config + * @param {{ + * src?: string, + * bundle?: boolean, + * key?: string, + * files?: { + * src: string, + * }[] + * }[]} libs + * @param {string} [filePattern] */ - bundle(params) { - let contents = ''; + constructor(config, libs, filePattern) { + this.config = config; + this.libs = libs; + this.mainBundleFiles = []; + this.filePattern = filePattern || 'client/lib/espo-{*}.min.js'; + } - if (params.patterns) { - let chunks = (new Bundler()).bundle({ - files: params.files, - patterns: params.patterns, - allPatterns: params.allPatterns, - libs: params.libs, + /** + * @return {Object.} + */ + bundle() { + let result = {}; + let mapping = {}; + let files = []; + let modules = []; + let templateFiles = []; + let mainName = this.config.order[0]; + + /** @var {Object.} */ + let notBundledMap = {}; + + this.config.order.forEach((name, i) => { + let data = this.#bundleChunk(name, i === 0, { + files: files, + templateFiles: templateFiles, }); - contents += chunks[0]; + files = files.concat(data.files); + templateFiles = templateFiles.concat(data.templateFiles); + modules = modules.concat(data.modules); + notBundledMap[name] = data.notBundledModules; + result[name] = data.contents; + + if (i === 0) { + return; + } + + data.modules.forEach(item => mapping[item] = name); + + let bundleFile = this.filePattern.replace('{*}', name); + + let libs = this.config.chunks[name].libs; + + if (libs) { + let part = JSON.stringify(libs.map(item => 'lib!' + item)); + + result[mainName] += `Espo.loader.mapBundleDependencies('${name}', ${part});\n`; + } + + result[mainName] += `Espo.loader.mapBundleFile('${name}', '${bundleFile}');\n`; + }); + + let notBundledModules = []; + + this.config.order.forEach(name => { + notBundledMap[name] + .filter(item => !modules.includes(item)) + .filter(item => !notBundledModules.includes(item)) + .forEach(item => notBundledModules.push(item)); + }); + + if (notBundledModules.length) { + let part = notBundledModules + .map(item => ' ' + item) + .join('\n'); + + console.log(`\nNot bundled:\n${part}`); + } + + result[mainName] += `Espo.loader.addBundleMapping(${JSON.stringify(mapping)});` + + return result; + } + + /** + * @param {string} name + * @param {boolean} isMain + * @param {{files: [], templateFiles: []}} alreadyBundled + * @return {{ + * contents: string, + * modules: string[], + * files: string[], + * templateFiles: string[], + * notBundledModules: string[], + * }} + */ + #bundleChunk(name, isMain, alreadyBundled) { + let contents = ''; + + let modules = []; + + let params = this.config.chunks[name]; + + let patterns = params.patterns; + let allPatterns = [] + .concat(this.config.allPatterns) + .concat(params.allPatterns || []); + + let bundledFiles = []; + let bundledTemplateFiles = []; + let notBundledModules = []; + + if (params.patterns) { + let bundler = (new Bundler(this.config.modulePaths)); + + // The main bundle is always loaded, duplicates are not needed. + let ignoreFiles = [].concat(this.mainBundleFiles); + + if (params.noDuplicates) { + ignoreFiles = ignoreFiles.concat(alreadyBundled.files); + } + + let data = bundler.bundle({ + files: params.files, + patterns: patterns, + allPatterns: allPatterns, + libs: this.libs, + ignoreFiles: ignoreFiles, + dependentOn: params.dependentOn, + }); + + contents += data.contents; + + if (isMain) { + this.mainBundleFiles = data.files; + } + + modules = data.modules; + bundledFiles = data.files; + + /*if (params.libs) { + contents = this.#bundleLibs(params.libs) + '\n' + contents; + }*/ + + notBundledModules = data.notBundledModules; } if (params.templatePatterns) { - contents += '\n' + - (new Precompiler()).precompile({ - patterns: params.templatePatterns, - modulePaths: params.modulePaths, - }); + let ignoreFiles = params.noDuplicates ? [].concat(alreadyBundled.templateFiles) : []; + + let data = (new Precompiler()).precompile({ + patterns: params.templatePatterns, + modulePaths: this.config.modulePaths, + ignoreFiles: ignoreFiles, + }); + + contents += '\n' + data.contents; + bundledTemplateFiles = data.files; } - return contents; + return { + contents: contents, + modules: modules, + files: bundledFiles, + templateFiles: bundledTemplateFiles, + notBundledModules: notBundledModules, + }; + } + + /** + * @param {string[]} libs + * @return {string} + */ + #bundleLibs(libs) { + let files = []; + + this.libs + .filter(item => libs.includes(item.key)) + .forEach(item => { + if (item.src) { + files.push(item.src); + + return; + } + + if (!item.files) { + return; + } + + item.files.forEach(item => { + files.push(item.src); + }); + }); + + let contents = files.map(file => fs.readFileSync(file, 'utf-8')); + + return contents.join('\n'); } } diff --git a/js/bundler.js b/js/bundler.js index 836972eb01..a7d588513c 100644 --- a/js/bundler.js +++ b/js/bundler.js @@ -38,6 +38,13 @@ const {globSync} = require('glob'); */ class Bundler { + /** + * @param {Object.} modulePaths + */ + constructor(modulePaths) { + this.modulePaths = modulePaths; + } + /** * @private * @type {string} @@ -48,55 +55,60 @@ class Bundler { * Bundles Espo js files into chunks. * * @param {{ - * files: string[], + * files?: string[], * patterns: string[], - * allPatterns: string[], - * chunkNumber?: number, + * allPatterns?: string[], + * ignoreFiles?: string[], + * dependentOn?: string[], * libs: { * src?: string, * bundle?: boolean, * key?: string, * }[], * }} params - * @return {string[]} + * @return {{ + * contents: string, + * files: string[], + * modules: string[], + * notBundledModules: string[], + * }} */ bundle(params) { - let chunkNumber = params.chunkNumber || 1; - let files = [] - .concat(params.files) - .concat(this.#obtainFiles(params.patterns, params.files)); + .concat(params.files || []) + .concat(this.#obtainFiles(params.patterns, params.files)) + .filter(file => !params.ignoreFiles.includes(file)); - let allFiles = this.#obtainFiles(params.allPatterns); + let allFiles = this.#obtainFiles(params.allPatterns || params.patterns); let ignoreLibs = params.libs .filter(item => item.key && !item.bundle) - .map(item => 'lib!' + item.key); + .map(item => 'lib!' + item.key) + .filter(item => !(params.dependentOn || []).includes(item)); - let sortedFiles = this.#sortFiles(files, allFiles, ignoreLibs); + let notBundledModules = []; - let portions = []; - let portionSize = Math.floor(sortedFiles.length / chunkNumber); + let sortedFiles = this.#sortFiles( + files, + allFiles, + ignoreLibs, + params.ignoreFiles || [], + notBundledModules, + params.dependentOn || null, + ); - for (let i = 0; i < chunkNumber; i++) { - let end = i === chunkNumber - 1 ? - sortedFiles.length : - (i + 1) * portionSize; + let contents = ''; - portions.push(sortedFiles.slice(i * portionSize, end)); - } + sortedFiles.forEach(file => contents += this.#normalizeSourceFile(file)); - let chunks = []; + let modules = sortedFiles.map(file => this.#obtainModuleName(file)); - portions.forEach(portion => { - let chunk = ''; - - portion.forEach(file => chunk += this.normalizeSourceFile(file)); - - chunks.push(chunk); - }); - - return chunks; + return { + contents: contents, + files: sortedFiles, + modules: modules, + notBundledModules: notBundledModules, + }; } /** @@ -123,17 +135,27 @@ class Bundler { * @param {string[]} files * @param {string[]} allFiles * @param {string[]} ignoreLibs + * @param {string[]} ignoreFiles + * @param {string[]} notBundledModules + * @param {string[]|null} dependentOn * @return {string[]} */ - #sortFiles(files, allFiles, ignoreLibs) { + #sortFiles( + files, + allFiles, + ignoreLibs, + ignoreFiles, + notBundledModules, + dependentOn + ) { /** @var {Object.} */ let map = {}; - let standalonePathList = []; - let modules = []; let moduleFileMap = {}; + let ignoreModules = ignoreFiles.map(file => this.#obtainModuleName(file)); + allFiles.forEach(file => { let data = this.#obtainModuleData(file); @@ -170,12 +192,16 @@ class Bundler { }); }); - modules = modules.concat(depModules); + modules = modules + .concat(depModules) + .filter(module => !ignoreModules.includes(module)); /** @var {string[]} */ let discardedModules = []; /** @var {Object.} */ let depthMap = {}; + /** @var {string[]} */ + let pickedModules = []; for (let name of modules) { this.#buildTreeItem( @@ -183,17 +209,29 @@ class Bundler { map, depthMap, ignoreLibs, - discardedModules + dependentOn, + discardedModules, + pickedModules ); } + if (dependentOn) { + modules = pickedModules; + } + modules.sort((v1, v2) => { return depthMap[v2] - depthMap[v1]; }); + discardedModules.forEach(item => notBundledModules.push(item)); + modules = modules.filter(item => !discardedModules.includes(item)); let modulePaths = modules.map(name => { + if (!moduleFileMap[name]) { + throw Error(`Can't obtain ${name}. Might be missing in allPatterns.`); + } + return moduleFileMap[name]; }); @@ -232,7 +270,9 @@ class Bundler { * @param {Object.} map * @param {Object.} depthMap * @param {string[]} ignoreLibs + * @param {string[]} dependentOn * @param {string[]} discardedModules + * @param {string[]} pickedModules * @param {number} [depth] * @param {string[]} [path] */ @@ -241,7 +281,9 @@ class Bundler { map, depthMap, ignoreLibs, + dependentOn, discardedModules, + pickedModules, depth, path ) { @@ -271,6 +313,12 @@ class Bundler { return; } + + if (dependentOn && dependentOn.includes(depName)) { + path + .filter(item => !pickedModules.includes(item)) + .forEach(item => pickedModules.push(item)); + } } deps.forEach(depName => { @@ -283,13 +331,31 @@ class Bundler { map, depthMap, ignoreLibs, + dependentOn, discardedModules, + pickedModules, depth + 1, path ); }); } + /** + * @param {string} file + * @return string + */ + #obtainModuleName(file) { + for (let mod in this.modulePaths) { + let part = this.modulePaths[mod] + '/src/'; + + if (file.indexOf(part) === 0) { + return mod + ':' + file.substring(part.length, file.length - 3); + } + } + + return file.slice(this.#getBathPath().length, -3); + } + /** * @param {string} path * @return {{deps: string[], name: string}|null} @@ -315,7 +381,7 @@ class Bundler { return null; } - let moduleName = path.slice(this._getBathPath().length, -3); + let moduleName = this.#obtainModuleName(path); let deps = []; @@ -344,7 +410,25 @@ class Bundler { * @return {boolean} */ #isClientJsFile(path) { - return path.indexOf(this._getBathPath()) === 0 && path.slice(-3) === '.js'; + if (path.slice(-3) !== '.js') { + return false; + } + + let startParts = [this.#getBathPath()]; + + for (let mod in this.modulePaths) { + let modPath = this.modulePaths[mod]; + + startParts.push(modPath); + } + + for (let starPart of startParts) { + if (path.indexOf(starPart) === 0) { + return true; + } + } + + return false; } /** @@ -352,9 +436,9 @@ class Bundler { * @param {string} path * @return {string} */ - normalizeSourceFile(path) { + #normalizeSourceFile(path) { let sourceCode = fs.readFileSync(path, 'utf-8'); - let basePath = this._getBathPath(); + let basePath = this.#getBathPath(); if (!this.#isClientJsFile(path)) { return sourceCode; @@ -401,7 +485,7 @@ class Bundler { * @private * @return {string} */ - _getBathPath() { + #getBathPath() { let path = this.basePath; if (path.slice(-1) !== '/') { diff --git a/js/diff.js b/js/diff.js index 31d0222f72..64a90e1bd3 100644 --- a/js/diff.js +++ b/js/diff.js @@ -32,6 +32,8 @@ const archiver = require('archiver'); const process = require('process'); const buildUtils = require('./build-utils'); +const bundleConfig = require('../frontend/bundle-config.json'); + const exec = cp.exec; /** @@ -281,10 +283,14 @@ class Diff fileList.push('client/lib/espo-libs.min.js'); fileList.push('client/lib/espo-libs.min.js.map'); - fileList.push(`client/lib/espo.min.js`); - fileList.push(`client/lib/espo.min.js.map`); - fileList.push('client/lib/espo-templates.min.js'); - fileList.push('client/lib/espo-templates.min.js.map'); + + Object.keys(bundleConfig.chunks) + .map(name => { + let namePart = name === 'main' ? 'espo' : 'espo-' + name; + + fileList.push(`client/lib/${namePart}.min.js`); + fileList.push(`client/lib/${namePart}.min.js.map`); + }); fs.readdirSync('client/css/espo/').forEach(file => { fileList.push('client/css/espo/' + file); diff --git a/js/scripts/prepare-lib-original.js b/js/scripts/prepare-lib-original.js index 303da92743..1929e69f59 100644 --- a/js/scripts/prepare-lib-original.js +++ b/js/scripts/prepare-lib-original.js @@ -30,6 +30,7 @@ const fs = require('fs'); const buildUtils = require('../build-utils'); const libs = require('./../../frontend/libs.json'); +const bundleConfig = require('../../frontend/bundle-config.json'); const libDir = './client/lib'; const originalLibDir = './client/lib/original'; @@ -40,10 +41,12 @@ const originalLibCrmDir = './client/modules/crm/lib/original'; .filter(path => !fs.existsSync(path)) .forEach(path => fs.mkdirSync(path)); -let bundleFiles = [ - 'espo-templates.js', - 'espo.js', -]; +let bundleFiles = Object.keys(bundleConfig.chunks) + .map(name => { + let namePart = name === 'main' ? 'espo' : 'espo-' + name; + + return namePart + '.js'; + }); fs.readdirSync(originalLibDir) .filter(file => !bundleFiles.includes(file)) diff --git a/js/template-precompiler.js b/js/template-precompiler.js index 30388e9a3e..61c70539f2 100644 --- a/js/template-precompiler.js +++ b/js/template-precompiler.js @@ -37,9 +37,10 @@ class TemplatePrecompiler { /** * @param {{ * patterns: string[], - * modulePaths: Object., + * modulePaths: Record., + * ignoreFiles: string[], * }} params - * @return {string} + * @return {{contents: string, files: string[]}} */ precompile(params) { let files = []; @@ -52,10 +53,15 @@ class TemplatePrecompiler { }); let nameMap = {}; + let compiledFiles = []; files.forEach(file => { let module = null; + if (params.ignoreFiles.includes(file)) { + return; + } + for (let itemModule in params.modulePaths) { let path = params.modulePaths[itemModule]; @@ -79,6 +85,7 @@ class TemplatePrecompiler { } nameMap[file] = name + compiledFiles.push(file); }); let contents = @@ -96,7 +103,10 @@ class TemplatePrecompiler { contents += `\n});`; - return contents; + return { + files: compiledFiles, + contents: contents, + }; } }