diff --git a/Gruntfile.js b/Gruntfile.js index 057d7c3184..ba7cf8c6ab 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -44,7 +44,13 @@ module.exports = grunt => { const originalLibDir = 'client/lib/original'; - let bundleFileMap = {'client/lib/espo-libs.min.js': buildUtils.getPreparedBundleLibList(libs)}; + let libsBundleFileList = [ + 'client/src/namespace.js', + 'client/src/loader.js', + ...buildUtils.getPreparedBundleLibList(libs), + ]; + + let bundleFileMap = {'client/lib/espo-libs.min.js': libsBundleFileList}; for (let name in bundleConfig.chunks) { let namePart = 'espo-' + name; diff --git a/application/Espo/Core/Utils/ClientManager.php b/application/Espo/Core/Utils/ClientManager.php index d80900c9f5..b3766539b4 100644 --- a/application/Espo/Core/Utils/ClientManager.php +++ b/application/Espo/Core/Utils/ClientManager.php @@ -42,8 +42,8 @@ use Slim\ResponseEmitter; */ class ClientManager { - protected string $mainHtmlFilePath = 'html/main.html'; - protected string $runScript = "app.start();"; + private string $mainHtmlFilePath = 'html/main.html'; + private string $runScript = "app.start();"; private string $favicon = 'client/img/favicon.ico'; private string $favicon196 = 'client/img/favicon196x196.png'; private string $basePath = ''; @@ -59,7 +59,6 @@ class ClientManager private DevModeJsFileListProvider $devModeJsFileListProvider, private Module $module ) { - $this->nonce = Util::generateKey(); } @@ -207,12 +206,13 @@ class ClientManager 'internalModuleList' => Json::encode($internalModuleList), 'applicationDescription' => $this->config->get('applicationDescription') ?? self::APP_DESCRIPTION, 'nonce' => $this->nonce, - 'jsLibs' => Json::encode($jsLibs), 'loaderParams' => Json::encode([ 'basePath' => $this->basePath, 'cacheTimestamp' => $loaderCacheTimestamp, 'internalModuleList' => $internalModuleList, 'transpiledModuleList' => $this->getTranspiledModuleList(), + 'libsConfig' => (object) $jsLibs, + 'aliasMap' => $this->getAliasMap(), ]), ]; @@ -240,8 +240,8 @@ class ClientManager { if ($this->isDeveloperMode()) { return array_merge( - $this->getDeveloperModeBundleLibFileList(), $this->metadata->get(['app', 'client', 'developerModeScriptList']) ?? [], + $this->getDeveloperModeBundleLibFileList(), ); } @@ -258,17 +258,17 @@ class ClientManager private function isDeveloperMode(): bool { - return $this->config->get('isDeveloperMode'); + return (bool) $this->config->get('isDeveloperMode'); } private function useCache(): bool { - return $this->config->get('useCache'); + return (bool) $this->config->get('useCache'); } private function useCacheInDeveloperMode(): bool { - return $this->config->get('useCacheInDeveloperMode'); + return (bool) $this->config->get('useCacheInDeveloperMode'); } private function getCacheTimestamp(): int @@ -359,4 +359,19 @@ class ClientManager $modules ); } + + private function getAliasMap(): object + { + $map = (object) []; + + foreach ($this->metadata->get(['app', 'jsLibs'], []) as $key => $item) { + $id = $item['amdId'] ?? null; + + if ($id) { + $map->$id = 'lib!' . $key; + } + } + + return $map; + } } diff --git a/application/Espo/Modules/Crm/Resources/metadata/app/jsLibs.json b/application/Espo/Modules/Crm/Resources/metadata/app/jsLibs.json index 91225813c6..53764e9e5e 100644 --- a/application/Espo/Modules/Crm/Resources/metadata/app/jsLibs.json +++ b/application/Espo/Modules/Crm/Resources/metadata/app/jsLibs.json @@ -9,7 +9,6 @@ "vis": { "path": "client/modules/crm/lib/vis.js", "devPath": "client/modules/crm/lib/original/vis.js", - "exportsAs": "vis", "sourceMap": true } } diff --git a/application/Espo/Resources/metadata/app/client.json b/application/Espo/Resources/metadata/app/client.json index a0bfba2e4d..bcef63ca3f 100644 --- a/application/Espo/Resources/metadata/app/client.json +++ b/application/Espo/Resources/metadata/app/client.json @@ -4,8 +4,7 @@ "client/lib/espo.min.js" ], "developerModeScriptList": [ - "client/src/loader.js", - "client/src/loader-init-libs.js" + "client/src/loader.js" ], "linkList": [ { diff --git a/application/Espo/Resources/metadata/app/jsLibs.json b/application/Espo/Resources/metadata/app/jsLibs.json index 4068bde144..5755a36116 100644 --- a/application/Espo/Resources/metadata/app/jsLibs.json +++ b/application/Espo/Resources/metadata/app/jsLibs.json @@ -1,4 +1,44 @@ { + "espo": { + "exportsTo": "window", + "exportsAs": "Espo" + }, + "jquery": { + "expose": true, + "exposeAs": "$", + "amdId": "jquery" + }, + "backbone": { + "exportsTo": "window", + "exportsAs": "Backbone" + }, + "bullbone": { + "expose": true, + "exposeAs": "Bull", + "amdId": "bullbone" + }, + "handlebars": { + "expose": true, + "exposeAs": "Handlebars" + }, + "underscore": { + "expose": true, + "exposeAs": "_", + "amdId": "underscore" + }, + "marked": {}, + "dompurify": { + "expose": true, + "exposeAs": "DOMPurify" + }, + "base64": { + "exportsTo": "window", + "exportsAs": "Base64" + }, + "moment": { + "exportsTo": "window", + "exportsAs": "moment" + }, "Flotr": { "path": "client/lib/flotr2.js", "devPath": "client/lib/original/flotr2.js", @@ -26,22 +66,30 @@ "exportsTo": "$.fn", "exportsAs": "textcomplete" }, + "autocomplete": { + "exportsTo": "$.fn", + "exportsAs": "autocomplete" + }, + "timepicker": { + "exportsTo": "$.fn", + "exportsAs": "timepicker" + }, + "bootstrap-datepicker": { + "exportsTo": "$.fn", + "exportsAs": "datepicker" + }, "Selectize": { "path": "client/lib/selectize.js", "devPath": "client/lib/original/selectize.js", "exportsTo": "window", "exportsAs": "Selectize" }, - "autonumeric": { - "exportsTo": "window", - "exportsAs": "AutoNumeric" - }, + "autonumeric": {}, "cronstrue": { "path": "client/lib/cronstrue-i18n.js", "devPath": "client/lib/original/cronstrue-i18n.js", - "exportsTo": "window", - "exportsAs": "cronstrue", - "sourceMap": true + "sourceMap": true, + "amdId": "cronstrue" }, "Cropper": { "path": "client/lib/cropper.js", @@ -61,8 +109,7 @@ "exif": { "path": "client/lib/exif.js", "devPath": "client/lib/original/exif.js", - "exportsTo": "window", - "exportsAs": "EXIF", + "amdId": "exif-js", "sourceMap": true }, "JsBarcode": { diff --git a/client/src/app.js b/client/src/app.js index d2a834522c..7e9afb46b2 100644 --- a/client/src/app.js +++ b/client/src/app.js @@ -32,7 +32,7 @@ import Espo from 'lib!espo'; import $ from 'lib!jquery'; import Backbone from 'lib!backbone'; import _ from 'lib!underscore'; -import Bull from 'lib!bullbone'; +import {Events, View as BullView, Factory as BullFactory} from 'lib!bullbone'; import Base64 from 'lib!base64'; import Ui from 'ui'; import Utils from 'utils'; @@ -811,7 +811,7 @@ class App { return 'client/custom/modules/' + mod + '/' + getResourceInnerPath(type, path); }; - this.viewFactory = new Bull.Factory({ + this.viewFactory = new BullFactory({ useCache: false, defaultViewName: 'views/base', helper: helper, @@ -1400,8 +1400,8 @@ class App { * @property {Number|null} [cacheTimestamp] A cache timestamp. */ -_.extend(App.prototype, Bull.Events); +_.extend(App.prototype, Events); -App.extend = Bull.View.extend; +App.extend = BullView.extend; export default App; diff --git a/client/src/collection.js b/client/src/collection.js index e142d0b41a..80de9f8220 100644 --- a/client/src/collection.js +++ b/client/src/collection.js @@ -29,7 +29,7 @@ /** @module collection */ import Model from 'model'; -import Bull from 'lib!bullbone'; +import {Events as BullEvents, View as BullView} from 'lib!bullbone'; import _ from 'lib!underscore'; /** @@ -1037,8 +1037,8 @@ class Collection { } } -Collection.extend = Bull.View.extend; -_.extend(Collection.prototype, Bull.Events); +Collection.extend = BullView.extend; +_.extend(Collection.prototype, BullEvents); const setOptions = { add: true, diff --git a/client/src/controller.js b/client/src/controller.js index 219f1271d0..1c66a6bb7c 100644 --- a/client/src/controller.js +++ b/client/src/controller.js @@ -29,7 +29,7 @@ /** @module controller */ import Exceptions from 'exceptions'; -import Bull from 'lib!bullbone'; +import {Events, View as BullView} from 'lib!bullbone'; import _ from 'lib!underscore'; /** @@ -612,8 +612,8 @@ class Controller { } /** For backward compatibility. */ -Controller.extend = Bull.View.extend; +Controller.extend = BullView.extend; -_.extend(Controller.prototype, Bull.Events); +_.extend(Controller.prototype, Events); export default Controller; diff --git a/client/src/handlers/working-time-range.js b/client/src/handlers/working-time-range.js index 9a5f55e8f5..d7a6254f49 100644 --- a/client/src/handlers/working-time-range.js +++ b/client/src/handlers/working-time-range.js @@ -26,7 +26,7 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -import Bull from 'lib!bullbone'; +import {Events} from 'lib!bullbone'; /** * @mixes Bull.Events @@ -49,6 +49,6 @@ class WorkingTimeRangeHandler { } } -_.extend(WorkingTimeRangeHandler.prototype, Bull.Events); +_.extend(WorkingTimeRangeHandler.prototype, Events); export default WorkingTimeRangeHandler; diff --git a/client/src/language.js b/client/src/language.js index ab92a3b825..264cab5135 100644 --- a/client/src/language.js +++ b/client/src/language.js @@ -28,7 +28,7 @@ /** @module language */ -import Bull from 'lib!bullbone'; +import {Events} from 'lib!bullbone'; /** * A language. @@ -322,6 +322,6 @@ class Language { } } -_.extend(Language.prototype, Bull.Events); +_.extend(Language.prototype, Events); export default Language; diff --git a/client/src/layout-manager.js b/client/src/layout-manager.js index 025cc0282e..e5ac9bf797 100644 --- a/client/src/layout-manager.js +++ b/client/src/layout-manager.js @@ -28,7 +28,7 @@ /** @module layout-manager */ -import Bull from 'lib!bullbone'; +import {Events} from 'lib!bullbone'; /** * A layout manager. @@ -276,6 +276,6 @@ class LayoutManager { } } -_.extend(LayoutManager.prototype, Bull.Events); +_.extend(LayoutManager.prototype, Events); export default LayoutManager; diff --git a/client/src/loader.js b/client/src/loader.js index 251e4e10de..6eb414ec7b 100644 --- a/client/src/loader.js +++ b/client/src/loader.js @@ -38,11 +38,23 @@ * A callback with resolved dependencies passed as parameters. * Should return a value to define a module. * - * @callback Espo.Loader~requireCallback + * @callback Loader~requireCallback * @param {...any} arguments Resolved dependencies. * @returns {*} */ + /** + * @typedef Loader~libData + * @type {Object} + * @property {string} [exportsTo] Exports to. + * @property {string} [exportsAs] Exports as. + * @property {boolean} [sourceMap] Has a source map. + * @property {boolean} [expose] To expose to global. + * @property {string} [exposeAs] To expose to global as. + * @property {string} [path] A path. + * @property {string} [devPath] A path in developer mode. + */ + /** * A loader. Used for loading and defining AMD modules, resource loading. * Handles caching. @@ -56,12 +68,14 @@ constructor(cache, _cacheTimestamp) { this._cacheTimestamp = _cacheTimestamp || null; this._cache = cache || null; + /** @type {Object.} */ this._libsConfig = {}; this._loadCallbacks = {}; this._pathsBeingLoaded = {}; this._dataLoaded = {}; - this._classMap = {}; - this._loadingSubject = null; + this._definedMap = {}; + this._aliasMap = {}; + this._contextId = null; this._responseCache = null; this._basePath = ''; @@ -172,31 +186,48 @@ /** * @private + * @param {string} id */ - _getClass(name) { - if (name in this._classMap) { - return this._classMap[name]; + _get(id) { + if (id in this._definedMap) { + return this._definedMap[id]; } - return false; + return void 0; } /** * @private + * @param {string} id + * @param {*} value */ - _setClass(name, o) { - this._classMap[name] = o; + _set(id, value) { + this._definedMap[id] = value; + + if (id.slice(0, 4) === 'lib!') { + let libName = id.slice(4); + + const libsData = this._libsConfig[libName]; + + if (libsData && libsData.expose) { + let key = libsData.exposeAs || libName; + + window[key] = value; + } + } } /** * @private + * @param {string} id + * @return {string} */ - _nameToPath(name) { - if (name.indexOf(':') === -1) { - return 'client/lib/transpiled/src/' + name + '.js'; + _idToPath(id) { + if (id.indexOf(':') === -1) { + return 'client/lib/transpiled/src/' + id + '.js'; } - let arr = name.split(':'); + let arr = id.split(':'); let namePart = arr[1]; let mod = arr[0]; @@ -218,25 +249,25 @@ /** * @private * @param {string} script - * @param {string} name + * @param {string} id * @param {string} path */ - _execute(script, name, path) { + _execute(script, id, path) { /** @var {?string} */ let module = null; - let colonIndex = name.indexOf(':'); + let colonIndex = id.indexOf(':'); if (colonIndex > 0) { - module = name.substring(0, colonIndex); + module = id.substring(0, colonIndex); } let noStrictMode = false; - if (!module && name.indexOf('lib!') === 0) { + if (!module && id.indexOf('lib!') === 0) { noStrictMode = true; - let realName = name.substring(4); + let realName = id.substring(4); let libsData = this._libsConfig[realName] || {}; @@ -272,100 +303,117 @@ /** * @private + * @param {string} id + * @param {*} value */ - _executeLoadCallback(subject, o) { - if (subject in this._loadCallbacks) { - this._loadCallbacks[subject].forEach(callback => callback(o)); - - delete this._loadCallbacks[subject]; + _executeLoadCallback(id, value) { + if (!(id in this._loadCallbacks)) { + return; } + + this._loadCallbacks[id].forEach(callback => callback(value)); + + delete this._loadCallbacks[id]; } /** * Define a module. * - * @param {string} subject A module name to be defined. - * @param {string[]} dependency A dependency list. - * @param {Espo.Loader~requireCallback} callback A callback with resolved dependencies + * @param {string|null} id A module name to be defined. + * @param {string[]} dependencyIds A dependency list. + * @param {Loader~requireCallback} callback A callback with resolved dependencies * passed as parameters. Should return a value to define the module. */ - define(subject, dependency, callback) { - if (subject) { - subject = this._normalizeClassName(subject); + define(id, dependencyIds, callback) { + if (id) { + id = this._normalizeClassName(id); } - if (this._loadingSubject) { - subject = subject || this._loadingSubject; + if (this._contextId) { + id = id || this._contextId; - this._loadingSubject = null; + this._contextId = null; } - if (this._getClass(subject)) { + let existing = this._get(id); + + if (typeof existing !== 'undefined') { return; } - if (!dependency) { - this._defineProceed(callback, subject, [], -1); + if (!dependencyIds) { + this._defineProceed(callback, id, [], -1); return; } - let indexOfExports = dependency.indexOf('exports'); + let indexOfExports = dependencyIds.indexOf('exports'); - this.require(dependency, (...args) => { - this._defineProceed(callback, subject, args, indexOfExports); + if (Array.isArray(dependencyIds)) { + dependencyIds = dependencyIds.map(item => this._normalizePath(item, id)); + } + + this.require(dependencyIds, (...args) => { + this._defineProceed(callback, id, args, indexOfExports); }); } /** * @private * @param {function} callback - * @param {string} subject + * @param {string} id * @param {Array} args * @param {number} indexOfExports */ - _defineProceed(callback, subject, args, indexOfExports) { - let o = callback.apply(root, args); + _defineProceed(callback, id, args, indexOfExports) { + let value = callback.apply(root, args); - if (!o && indexOfExports === -1) { + if (typeof value === 'undefined' && indexOfExports === -1 && id) { if (this._cache) { - this._cache.clear('a', subject); + this._cache.clear('a', id); } - throw new Error("Could not load '" + subject + "'"); + throw new Error("Could not load '" + id + "'"); } if (indexOfExports !== -1) { let exports = args[indexOfExports]; - o = ('default' in exports) ? exports.default : exports; + value = ('default' in exports) ? exports.default : exports; } - this._setClass(subject, o); - this._executeLoadCallback(subject, o); + if (!id) { + console.error(value); + // Libs can define w/o id and set to the root. + // Not supposed to happen as should be suppressed by require.amd = false; + return; + } + + this._set(id, value); + this._executeLoadCallback(id, value); } /** * Require a module or multiple modules. * - * @param {string|string[]} subject A module or modules to require. - * @param {Espo.Loader~requireCallback} callback A callback with resolved dependencies. + * @param {string|string[]} id A module or modules to require. + * @param {Loader~requireCallback} callback A callback with resolved dependencies. * @param {Function|null} [errorCallback] An error callback. */ - require(subject, callback, errorCallback) { + require(id, callback, errorCallback) { let list; - if (Object.prototype.toString.call(subject) === '[object Array]') { - list = subject; + if (Object.prototype.toString.call(id) === '[object Array]') { + list = id; list.forEach((item, i) => { list[i] = this._normalizeClassName(item); }); } - else if (subject) { - subject = this._normalizeClassName(subject); + else if (id) { + id = this._normalizeClassName(id); - list = [subject]; + list = [id]; } else { list = []; @@ -418,11 +466,62 @@ return string.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); } + /** + * @param {string} path + * @param {string} id + * @private + */ + _normalizePath(path, id) { + if (path.at(0) !== '.') { + return path; + } + + if (path.slice(0, 2) !== './' && path.slice(0, 3) !== '../') { + return path; + } + + let outputPath = path; + + let dirParts = id.split('/').slice(0, -1); + + if (path.slice(0, 2) === './') { + outputPath = dirParts.join('/') + '/' + path.slice(2); + } + + let parts = outputPath.split('/'); + + let up = 0; + + for (let part of parts) { + if (part === '..') { + up++; + + continue; + } + + break; + } + + if (!up) { + return outputPath; + } + + if (up) { + outputPath = dirParts.slice(0, -up).join('/') + '/' + outputPath.slice(3 * up); + } + + return outputPath; + } + /** * @private */ _normalizeClassName(name) { - if (~name.indexOf('.') && !~name.indexOf('!')) { + if (name in this._aliasMap) { + name = this._aliasMap[name]; + } + + if (~name.indexOf('.') && !~name.indexOf('!') && !name.slice(-3) === '.js') { console.warn( name + ': ' + 'class name should use slashes for a directory separator and hyphen format.' @@ -484,7 +583,6 @@ let dataType, type, path, exportsTo, exportsAs; let realName = name; - let noAppCache = false; if (name.indexOf('lib!') === 0) { @@ -495,31 +593,49 @@ path = realName; exportsTo = 'window'; - exportsAs = realName; + exportsAs = null; - if (realName in this._libsConfig) { - let libData = this._libsConfig[realName] || {}; + + let isDefinedLib = realName in this._libsConfig; + + if (isDefinedLib) { + const libData = this._libsConfig[realName] || {}; path = libData.path || path; - if (this._isDeveloperMode) { - path = libData.devPath || path; + if (this._isDeveloperMode && libData.devPath) { + path = libData.devPath; } - exportsTo = libData.exportsTo || exportsTo; - exportsAs = libData.exportsAs || exportsAs; + path = (this._isDeveloperMode ? libData.devPath : libData.path) || path; + exportsTo = libData.exportsTo || null; + exportsAs = libData.exportsAs || null; + } + + if (isDefinedLib && !exportsTo) { + type = 'amd'; } if (path.indexOf(':') !== -1) { console.error(`Not allowed path '${path}'.`); + throw new Error(); } noAppCache = true; - let obj = this._fetchObject(exportsTo, exportsAs); + let obj = void 0; - if (obj) { + if (exportsTo && exportsAs) { + obj = this._fetchObject(exportsTo, exportsAs); + } + + + if (typeof obj === 'undefined' && name in this._definedMap) { + obj = this._definedMap[name]; + } + + if (typeof obj !== 'undefined') { callback(obj); return; @@ -534,20 +650,21 @@ if (path.indexOf(':') !== -1) { console.error(`Not allowed path '${path}'.`); + throw new Error(); } } else { dataType = 'script'; - type = 'class'; + type = 'amd'; if (!name || name === '') { throw new Error("Can not load empty class name"); } - let classObj = this._getClass(name); + let classObj = this._get(name); - if (classObj) { + if (typeof classObj !== 'undefined') { callback(classObj); return; @@ -557,9 +674,9 @@ let bundleName = this._bundleMapping[name]; this._requireBundle(bundleName).then(() => { - let classObj = this._getClass(name); + let classObj = this._get(name); - if (!classObj) { + if (typeof classObj === 'undefined') { let msg = `Could not obtain class '${name}' from bundle '${bundleName}'.`; console.error(msg); @@ -572,7 +689,7 @@ return; } - path = this._nameToPath(name); + path = this._idToPath(name); } if (name in this._dataLoaded) { @@ -749,31 +866,31 @@ * @private */ _processCached(dto, cached) { - let name = dto.name; + let id = dto.name; let callback = dto.callback; let type = dto.type; let dataType = dto.dataType; let exportsAs = dto.exportsAs; let exportsTo = dto.exportsTo; - if (type === 'class') { - this._loadingSubject = name; + if (type === 'amd') { + this._contextId = id; } if (dataType === 'script') { - this._execute(cached, name, dto.path); + this._execute(cached, id, dto.path); } - if (type === 'class') { - let classObj = this._getClass(name); + if (type === 'amd') { + let value = this._get(id); - if (classObj) { - callback(classObj); + if (typeof value !== 'undefined') { + callback(value); return; } - this._addLoadCallback(name, callback); + this._addLoadCallback(id, callback); return; } @@ -784,7 +901,7 @@ data = this._fetchObject(exportsTo, exportsAs); } - this._dataLoaded[name] = data; + this._dataLoaded[id] = data; callback(data); } @@ -793,13 +910,14 @@ * @private */ _processRequest(dto) { - let name = dto.name; + let id = dto.name; let url = dto.url; let errorCallback = dto.errorCallback; let path = dto.path; let useCache = dto.useCache; let noAppCache = dto.noAppCache; + // @todo Use `fetch`. $.ajax({ type: 'GET', cache: useCache, @@ -810,7 +928,7 @@ }) .then(response => { if (this._cache && !noAppCache && !this._responseCache) { - this._cache.set('a', name, response); + this._cache.set('a', id, response); } if (this._responseCache) { @@ -834,48 +952,58 @@ * @private */ _handleResponse(dto, response) { - let name = dto.name; + let id = dto.name; let callback = dto.callback; let type = dto.type; let dataType = dto.dataType; let exportsAs = dto.exportsAs; let exportsTo = dto.exportsTo; - this._addLoadCallback(name, callback); + this._addLoadCallback(id, callback); - if (type === 'class') { - this._loadingSubject = name; + if (type === 'amd') { + this._contextId = id; + } + + let isLib = id.slice(0, 4) === 'lib!'; + + if (isLib && exportsAs) { + define.amd = false; } if (dataType === 'script') { - this._execute(response, name, dto.path); + this._execute(response, id, dto.path); } - let data; + if (isLib && exportsAs) { + define.amd = true; + } - if (type === 'class') { - data = this._getClass(name); + let result; - if (data && typeof data === 'function') { - this._executeLoadCallback(name, data); + if (type === 'amd') { + result = this._get(id); + + if (typeof result !== 'undefined') { + this._executeLoadCallback(id, result); } return; } - data = response; + result = response; if (exportsTo && exportsAs) { - data = this._fetchObject(exportsTo, exportsAs); + result = this._fetchObject(exportsTo, exportsAs); } - this._dataLoaded[name] = data; + this._dataLoaded[id] = result; - this._executeLoadCallback(name, data); + this._executeLoadCallback(id, result); } /** - * @param {Object} data + * @param {Object.} data * @internal */ addLibsConfig(data) { @@ -888,6 +1016,13 @@ this._libsConfig = {...this._libsConfig, ...data}; } + /** + * @param {Object.} map + */ + setAliasMap(map) { + this._aliasMap = map; + } + /** * @private */ @@ -925,16 +1060,24 @@ Object.assign(this._bundleMapping, mapping); } + /** + * @param {string} id + * @internal + */ + setContextId(id) { + this._contextId = id; + } + /** * Require a module or multiple modules. * - * @param {...string} subject A module or modules to require. + * @param {...string} id A module or modules to require. * @returns {Promise} */ - requirePromise(subject) { + requirePromise(id) { return new Promise((resolve, reject) => { this.require( - subject, + id, (...args) => resolve(...args), () => reject() ); @@ -993,55 +1136,41 @@ loader.setResponseCache(responseCache); }, - /** - * @param {string[]} internalModuleList - */ - setInternalModuleList: function (internalModuleList) { - loader.setInternalModuleList(internalModuleList); - }, - - /** - * @param {string[]} transpiledModuleList - */ - setTranspiledModuleList: function (transpiledModuleList) { - loader.setTranspiledModuleList(transpiledModuleList); - }, - /** * Define a module. * - * @param {string} subject A module name to be defined. - * @param {string[]} dependency A dependency list. - * @param {Espo.Loader~requireCallback} callback A callback with resolved dependencies + * @param {string} id A module name to be defined. + * @param {string[]} dependencyIds A dependency list. + * @param {Loader~requireCallback} callback A callback with resolved dependencies * passed as parameters. Should return a value to define the module. */ - define: function (subject, dependency, callback) { - loader.define(subject, dependency, callback); + define: function (id, dependencyIds, callback) { + loader.define(id, dependencyIds, callback); }, /** * Require a module or multiple modules. * - * @param {string|string[]} subject A module or modules to require. - * @param {Espo.Loader~requireCallback} callback A callback with resolved dependencies. + * @param {string|string[]} id A module or modules to require. + * @param {Loader~requireCallback} callback A callback with resolved dependencies. * @param {Function|null} [errorCallback] An error callback. */ - require: function (subject, callback, errorCallback) { - loader.require(subject, callback, errorCallback); + require: function (id, callback, errorCallback) { + loader.require(id, callback, errorCallback); }, /** * Require a module or multiple modules. * - * @param {string|string[]} subject A module or modules to require. + * @param {string|string[]} id A module or modules to require. * @returns {Promise} */ - requirePromise: function (subject) { - return loader.requirePromise(subject); + requirePromise: function (id) { + return loader.requirePromise(id); }, /** - * @param {Object} data + * @param {Object.} data * @internal */ addLibsConfig: function (data) { @@ -1073,24 +1202,32 @@ addBundleMapping: function (mapping) { loader.addBundleMapping(mapping); }, + + /** + * @param {string} id + * @internal + */ + setContextId: function (id) { + loader.setContextId(id); + }, }; /** * Require a module or multiple modules. * - * @param {string|string[]} subject A module or modules to require. - * @param {Espo.Loader~requireCallback} callback A callback with resolved dependencies. + * @param {string|string[]} id A module or modules to require. + * @param {Loader~requireCallback} callback A callback with resolved dependencies. * @param {Object} [context] A context. * @param {Function|null} [errorCallback] An error callback. * * @deprecated Use `Espo.loader.require` instead. */ - root.require = Espo.require = function (subject, callback, context, errorCallback) { + root.require = Espo.require = function (id, callback, context, errorCallback) { if (context) { callback = callback.bind(context); } - loader.require(subject, callback, errorCallback); + loader.require(id, callback, errorCallback); }; /** @@ -1101,50 +1238,56 @@ * 2. `(dependencyList, callback)` – Unnamed, with dependencies. * 3. `(moduleName, dependencyList, callback)` – Named. * - * @param {string|string[]|Espo.Loader~requireCallback} arg1 A module name to be defined, + * @param {string|string[]|Loader~requireCallback} arg1 A module name to be defined, * a dependency list or a callback. - * @param {string[]|Espo.Loader~requireCallback} [arg2] A dependency list or a callback with resolved + * @param {string[]|Loader~requireCallback} [arg2] A dependency list or a callback with resolved * dependencies. - * @param {Espo.Loader~requireCallback} [arg3] A callback with resolved dependencies. + * @param {Loader~requireCallback} [arg3] A callback with resolved dependencies. */ root.define = Espo.define = function (arg1, arg2, arg3) { - let subject = null; - let dependency = null; + let id = null; + let depIds = null; let callback; if (typeof arg1 === 'function') { callback = arg1; } else if (typeof arg1 !== 'undefined' && typeof arg2 === 'function') { - dependency = arg1; + if (Array.isArray(arg1)) { + depIds = arg1; + } else { + id = arg1; + depIds = []; + } + callback = arg2; } else { - subject = arg1; - dependency = arg2; + id = arg1; + depIds = arg2; callback = arg3; } - loader.define(subject, dependency, callback); + loader.define(id, depIds, callback); }; + root.define.amd = true; + (() => { - let loaderParamsTag = document.querySelector('script[data-name="loader-params"]'); + const loaderParamsTag = document.querySelector('script[data-name="loader-params"]'); - if (loaderParamsTag) { - let params = JSON.parse(loaderParamsTag.textContent); - - Espo.loader.setCacheTimestamp(params.cacheTimestamp); - Espo.loader.setBasePath(params.basePath); - Espo.loader.setInternalModuleList(params.internalModuleList); - Espo.loader.setTranspiledModuleList(params.transpiledModuleList); + if (!loaderParamsTag) { + return; } - let jsLibsTag = document.querySelector('script[data-name="js-libs"]'); + const params = JSON.parse(loaderParamsTag.textContent); - if (jsLibsTag) { - Espo.loader.addLibsConfig(JSON.parse(jsLibsTag.textContent)); - } + loader.setCacheTimestamp(params.cacheTimestamp); + loader.setBasePath(params.basePath); + loader.setInternalModuleList(params.internalModuleList); + loader.setTranspiledModuleList(params.transpiledModuleList); + loader.addLibsConfig(params.libsConfig); + loader.setAliasMap(params.aliasMap); })(); }).call(window); diff --git a/client/src/metadata.js b/client/src/metadata.js index c58b4d0dfc..6b8f36086c 100644 --- a/client/src/metadata.js +++ b/client/src/metadata.js @@ -28,7 +28,7 @@ /** @module metadata */ -import Bull from 'lib!bullbone'; +import {Events} from 'lib!bullbone'; /** * Application metadata. @@ -261,6 +261,6 @@ class Metadata { } } -_.extend(Metadata.prototype, Bull.Events); +_.extend(Metadata.prototype, Events); export default Metadata; diff --git a/client/src/model.js b/client/src/model.js index c62f4bf127..abf81ad0c5 100644 --- a/client/src/model.js +++ b/client/src/model.js @@ -28,7 +28,7 @@ /** @module model */ -import Bull from 'lib!bullbone'; +import {Events, View as BullView} from 'lib!bullbone'; import _ from 'lib!underscore'; /** @@ -1008,8 +1008,8 @@ class Model { } } -Model.extend = Bull.View.extend; +Model.extend = BullView.extend; -_.extend(Model.prototype, Bull.Events); +_.extend(Model.prototype, Events); export default Model; diff --git a/client/src/storage.js b/client/src/storage.js index a7d6df3879..597a79606d 100644 --- a/client/src/storage.js +++ b/client/src/storage.js @@ -28,8 +28,6 @@ /** @module storage */ -import Bull from 'lib!bullbone'; - /** * A storage. Data is saved across browser sessions, has no expiration time. */ diff --git a/client/src/view-record-helper.js b/client/src/view-record-helper.js index 6f7a2701b9..bab3d136c0 100644 --- a/client/src/view-record-helper.js +++ b/client/src/view-record-helper.js @@ -28,7 +28,7 @@ /** @module view-record-helper */ -import Bull from 'lib!bullbone'; +import {Events} from 'lib!bullbone'; class ViewRecordHelper { @@ -207,6 +207,6 @@ class ViewRecordHelper { } } -_.extend(ViewRecordHelper.prototype, Bull.Events); +_.extend(ViewRecordHelper.prototype, Events); export default ViewRecordHelper; diff --git a/client/src/view.js b/client/src/view.js index dd724fe966..10e5fad752 100644 --- a/client/src/view.js +++ b/client/src/view.js @@ -28,14 +28,15 @@ /** @module view */ -import Bull from 'lib!bullbone'; +import {View as BullView} from 'lib!bullbone'; /** * A base view. All views should extend this class. * * @see {@link https://docs.espocrm.com/development/view/} + * @mixes Bull.Events */ -class View extends Bull.View { +class View extends BullView { /** * @callback module:view~actionHandlerCallback diff --git a/frontend/bundle-config.json b/frontend/bundle-config.json index 105060fe06..3e767b1cf8 100644 --- a/frontend/bundle-config.json +++ b/frontend/bundle-config.json @@ -15,12 +15,11 @@ "files": [ "src/namespace.js", "src/loader.js", - "src/loader-init-libs.js", "src/exceptions.js", "src/utils.js" ], "patterns": [ - "src/!(pre-loader).js", + "src/!(pre-loader|namespace|loader).js", "src/ui/**/*.js", "src/views/*.js", "src/views/site/*.js", diff --git a/frontend/libs.json b/frontend/libs.json index f1b44343b4..d9c7893cb6 100644 --- a/frontend/libs.json +++ b/frontend/libs.json @@ -5,7 +5,7 @@ "key": "jquery" }, { - "src": "node_modules/underscore/underscore.js", + "src": "node_modules/underscore/underscore-umd.js", "bundle": true, "key": "underscore" }, @@ -20,22 +20,27 @@ "key": "handlebars" }, { - "src": "node_modules/bullbone/dist/bullbone.js", + "src": "node_modules/bullbone/dist/bullbone.umd.js", "bundle": true, "key": "bullbone" }, { "src": "node_modules/js-base64/base64.js", "bundle": true, - "key": "Base64" + "key": "Base64", + "suppressAmd": true }, { "src": "node_modules/moment/moment.js", - "bundle": true + "bundle": true, + "key": "moment", + "suppressAmd": true }, { "src": "node_modules/moment-timezone/moment-timezone.js", - "bundle": true + "bundle": true, + "key": "moment-timezone", + "suppressAmd": true }, { "src": "node_modules/moment-timezone-data-build/build/moment-timezone-data.js", @@ -43,11 +48,15 @@ }, { "src": "node_modules/timepicker/jquery.timepicker.js", - "bundle": true + "bundle": true, + "key": "timepicker", + "suppressAmd": true }, { "src": "node_modules/devbridge-autocomplete/dist/jquery.autocomplete.js", - "bundle": true + "bundle": true, + "key": "autocomplete", + "suppressAmd": true }, { "src": "node_modules/jquery-textcomplete/dist/jquery.textcomplete.js", @@ -60,7 +69,9 @@ }, { "src": "node_modules/bootstrap-datepicker/dist/js/bootstrap-datepicker.js", - "bundle": true + "bundle": true, + "key": "bootstrap-datepicker", + "suppressAmd": true }, { "src": "node_modules/marked/lib/marked.umd.js", @@ -78,26 +89,26 @@ }, { "src": "node_modules/jquery-ui-espo/dist/jquery-ui.js", - "bundle": true + "bundle": true, + "suppressAmd": true }, { "src": "node_modules/jquery-ui-touch-punch/jquery.ui.touch-punch.js", - "bundle": true + "bundle": true, + "suppressAmd": true }, { "name": "gridstack", - "files": [ - { - "src": "node_modules/gridstack/dist/gridstack-jq.js" - } - ], + "src": "node_modules/gridstack/dist/gridstack-jq.js", "bundle": true, - "key": "gridstack" + "key": "gridstack", + "suppressAmd": true }, { "src": "node_modules/selectize/dist/js/standalone/selectize.js", "bundle": true, - "key": "Selectize" + "key": "Selectize", + "suppressAmd": true }, { "src": "node_modules/autonumeric/dist/autoNumeric.js", diff --git a/frontend/test/jasmine-browser.json b/frontend/test/jasmine-browser.json index 392a7f3bc9..e42fd90ce0 100644 --- a/frontend/test/jasmine-browser.json +++ b/frontend/test/jasmine-browser.json @@ -3,7 +3,7 @@ "srcFiles": [ "client/lib/espo-libs.min.js", "client/lib/transpiled/src/loader.js", - "client/lib/transpiled/src/loader-init-libs.js", + "frontend/test/loader-init-libs.js", "client/lib/transpiled/src/exceptions.js", "client/lib/transpiled/src/utils.js", "client/lib/transpiled/src/ajax.js", diff --git a/client/src/loader-init-libs.js b/frontend/test/loader-init-libs.js similarity index 56% rename from client/src/loader-init-libs.js rename to frontend/test/loader-init-libs.js index fcf456873c..d06c0abd6c 100644 --- a/client/src/loader-init-libs.js +++ b/frontend/test/loader-init-libs.js @@ -26,47 +26,44 @@ * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ -Espo.loader.addLibsConfig( - { - "espo": { - "exportsTo": "window", - "exportsAs": "Espo" - }, - "jquery": { - "exportsTo": "window", - "exportsAs": "$" - }, - "backbone": { - "exportsTo": "window", - "exportsAs": "Backbone" - }, - "bullbone": { - "exportsTo": "window", - "exportsAs": "Bull" - }, - "handlebars": { - "exportsTo": "window", - "exportsAs": "Handlebars" - }, - "underscore": { - "exportsTo": "window", - "exportsAs": "_" - }, - "marked": { - "exportsTo": "window", - "exportsAs": "marked" - }, - "dompurify": { - "exportsTo": "window", - "exportsAs": "DOMPurify" - }, - "base64": { - "exportsTo": "window", - "exportsAs": "Base64" - }, - "moment": { - "exportsTo": "window", - "exportsAs": "moment" - } +Espo.loader.addLibsConfig({ + "espo": { + "exportsTo": "window", + "exportsAs": "Espo" + }, + "jquery": { + "expose": true, + "exposeAs": "$", + "amdId": "jquery" + }, + "backbone": { + "exportsTo": "window", + "exportsAs": "Backbone" + }, + "bullbone": { + "expose": true, + "exposeAs": "Bull", + "amdId": "bullbone" + }, + "handlebars": { + "expose": true, + "exposeAs": "Handlebars" + }, + "underscore": { + "expose": true, + "exposeAs": "_", + "amdId": "underscore" + }, + "marked": {}, + "dompurify": { + "expose": true, + "exposeAs": "DOMPurify" + }, + "base64": { + "exportsTo": "window", + "exportsAs": "Base64" + }, + "moment": { + "expose": true } -); +}); diff --git a/html/main.html b/html/main.html index 173406a0a8..490cb13157 100644 --- a/html/main.html +++ b/html/main.html @@ -2,8 +2,7 @@ {{applicationName}} - - {{scriptsHtml}} + {{scriptsHtml}} {{additionalStyleSheetsHtml}}{{linksHtml}} diff --git a/js/scripts/prepare-lib-original.js b/js/scripts/prepare-lib-original.js index fd0b5ac5c1..d4bc028b28 100644 --- a/js/scripts/prepare-lib-original.js +++ b/js/scripts/prepare-lib-original.js @@ -55,10 +55,7 @@ fs.readdirSync(originalLibDir) fs.readdirSync(originalLibCrmDir) .forEach(file => fs.unlinkSync(originalLibCrmDir + '/' + file)); -/** @var {string[]} */ -const libSrcList = buildUtils.getBundleLibList(libs); - -let stripSourceMappingUrl = path => { +const stripSourceMappingUrl = path => { /** @var {string} */ let originalContents = fs.readFileSync(path, {encoding: 'utf-8'}); @@ -73,11 +70,65 @@ let stripSourceMappingUrl = path => { fs.writeFileSync(path, contents, {encoding: 'utf-8'}); } +const addLoadingSubject = (path, subject) => { + /** @var {string} */ + let contents = fs.readFileSync(path, {encoding: 'utf-8'}); + + contents = + `Espo.loader.setContextId('${subject}');\n` + + contents + '\n' + + `Espo.loader.setContextId(null);\n`; + + fs.writeFileSync(path, contents, {encoding: 'utf-8'}); +} + +const addSuppressAmd = path => { + /** @var {string} */ + let contents = fs.readFileSync(path, {encoding: 'utf-8'}); + + contents = + `var _previousDefineAmd = define.amd; define.amd = false;\n` + + contents + '\n' + + `define.amd = _previousDefineAmd;\n`; + + fs.writeFileSync(path, contents, {encoding: 'utf-8'}); +} + +/** @var {string[]} */ +const libSrcList = buildUtils.getBundleLibList(libs); + +let keyMap = {}; +let suppressAmdMap = {}; + +libs.forEach(item => { + if (!item.key || !item.bundle || item.files) { + return; + } + + if (item.suppressAmd) { + suppressAmdMap[item.src] = true; + + return; + } + + keyMap[item.src] = 'lib!' + item.key; +}); + libSrcList.forEach(src => { let dest = originalLibDir + '/' + src.split('/').slice(-1); fs.copyFileSync(src, dest); stripSourceMappingUrl(dest); + + if (suppressAmdMap[src]) { + addSuppressAmd(dest); + } + + let key = keyMap[src]; + + if (key) { + addLoadingSubject(dest, key); + } }); buildUtils.getCopyLibDataList(libs) @@ -85,4 +136,14 @@ buildUtils.getCopyLibDataList(libs) .forEach(item => { fs.copyFileSync(item.src, item.originalDest); stripSourceMappingUrl(item.originalDest); + + if (suppressAmdMap[item.src]) { + addSuppressAmd(item.originalDest); + } + + let key = keyMap[item.src]; + + if (key) { + addLoadingSubject(item.originalDest, key); + } }); diff --git a/package-lock.json b/package-lock.json index c19f5916a8..68f40e72b9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,7 @@ "bootstrap": "^3.4.1", "bootstrap-colorpicker": "^2.5.2", "bootstrap-datepicker": "^1.9.0", - "bullbone": "github:yurikuzn/bull#1.1.5", + "bullbone": "github:yurikuzn/bull#1.2.2", "cronstrue": "^1.114.0", "cropper": "^0.7.9", "devbridge-autocomplete": "^1.4.11", @@ -31,7 +31,7 @@ "handlebars": "^4.7.7", "jquery": "^3.6.0", "jquery-textcomplete": "^1.8.5", - "jquery-ui-espo": "github:yurikuzn/jquery-ui-espo#0.2.0", + "jquery-ui-espo": "github:yurikuzn/jquery-ui-espo#0.2.2", "jquery-ui-touch-punch": "^0.2.3", "js-base64": "^3.7.2", "jsbarcode": "^3.11.4", @@ -2542,8 +2542,8 @@ } }, "node_modules/bullbone": { - "version": "1.1.5", - "resolved": "git+ssh://git@github.com/yurikuzn/bull.git#219a5bef1b5442c17c58330d67a34e3e4b78341e", + "version": "1.2.2", + "resolved": "git+ssh://git@github.com/yurikuzn/bull.git#25eeb1a94f3ad86845f92bd979be008221075144", "license": "MIT" }, "node_modules/bytes": { @@ -4968,8 +4968,8 @@ } }, "node_modules/jquery-ui-espo": { - "version": "0.1.0", - "resolved": "git+ssh://git@github.com/yurikuzn/jquery-ui-espo.git#89b2d030d7b13f5f56a19e5f79776e7eb143f77e", + "version": "0.2.2", + "resolved": "git+ssh://git@github.com/yurikuzn/jquery-ui-espo.git#844897483dbf31603b81cadda07b0de5602c231e", "license": "ISC", "dependencies": { "grunt": "^1.4.1", @@ -8908,8 +8908,8 @@ "dev": true }, "bullbone": { - "version": "git+ssh://git@github.com/yurikuzn/bull.git#219a5bef1b5442c17c58330d67a34e3e4b78341e", - "from": "bullbone@github:yurikuzn/bull#1.1.5" + "version": "git+ssh://git@github.com/yurikuzn/bull.git#25eeb1a94f3ad86845f92bd979be008221075144", + "from": "bullbone@github:yurikuzn/bull#1.2.2" }, "bytes": { "version": "3.1.2", @@ -10706,8 +10706,8 @@ } }, "jquery-ui-espo": { - "version": "git+ssh://git@github.com/yurikuzn/jquery-ui-espo.git#89b2d030d7b13f5f56a19e5f79776e7eb143f77e", - "from": "jquery-ui-espo@github:yurikuzn/jquery-ui-espo#0.2.0", + "version": "git+ssh://git@github.com/yurikuzn/jquery-ui-espo.git#844897483dbf31603b81cadda07b0de5602c231e", + "from": "jquery-ui-espo@github:yurikuzn/jquery-ui-espo#0.2.2", "requires": { "grunt": "^1.4.1", "jquery-ui": "^1.13.0" diff --git a/package.json b/package.json index 668d2d5a6a..e93abead7a 100644 --- a/package.json +++ b/package.json @@ -47,7 +47,7 @@ "bootstrap": "^3.4.1", "bootstrap-colorpicker": "^2.5.2", "bootstrap-datepicker": "^1.9.0", - "bullbone": "github:yurikuzn/bull#1.1.5", + "bullbone": "github:yurikuzn/bull#1.2.2", "cronstrue": "^1.114.0", "cropper": "^0.7.9", "devbridge-autocomplete": "^1.4.11", @@ -61,7 +61,7 @@ "handlebars": "^4.7.7", "jquery": "^3.6.0", "jquery-textcomplete": "^1.8.5", - "jquery-ui-espo": "github:yurikuzn/jquery-ui-espo#0.2.0", + "jquery-ui-espo": "github:yurikuzn/jquery-ui-espo#0.2.2", "jquery-ui-touch-punch": "^0.2.3", "js-base64": "^3.7.2", "jsbarcode": "^3.11.4", diff --git a/schema/metadata/app/jsLibs.json b/schema/metadata/app/jsLibs.json index f4ec6fc39c..f66be9962a 100644 --- a/schema/metadata/app/jsLibs.json +++ b/schema/metadata/app/jsLibs.json @@ -24,14 +24,10 @@ }, "exportsTo": { "anyOf": [ - { - "type": "string" - }, - { - "enum": ["window", "$.fn"] - } + {"type": "string"}, + {"enum": ["window", "$.fn"]} ], - "description": "To where the lib is exported. Use dots to define a path." + "description": "If the lib exposes itself w/o a module, to where the lib is exported. Use dots to define a path." }, "exportsAs": { "type": "string", @@ -40,6 +36,18 @@ "sourceMap": { "type": "boolean", "description": "Whether the source map file is available." + }, + "expose": { + "type": "boolean", + "description": "To expose to the window global. To be used for backward compatibility reasons." + }, + "exposeAs": { + "type": "string", + "description": "A name to expose by. By default, matches the lib name." + }, + "amdId": { + "type": "string", + "description": "An AMD ID. If the module uses a named define function, specify that name." } } }