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,
+ };
}
}