From 8132884840b1bee4303b5a4d2ffc12918a259979 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Fri, 29 Mar 2019 10:59:30 +0000 Subject: [PATCH] [REF] web: make boot.js independent of libraries This is a first step towards lazy loading the majority of the JS: the boot JS code should not depend on any libraries (underscore.js and jQuery). Even without lazy loading in mind, this would be a nice-to-have. Part of https://github.com/odoo/odoo/pull/32181 task-1961045 --- addons/web/static/src/js/boot.js | 512 ++++++++++++++------------ addons/web/static/tests/boot_tests.js | 13 +- 2 files changed, 281 insertions(+), 244 deletions(-) diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js index cf1cbaba180..8498bf598cf 100644 --- a/addons/web/static/src/js/boot.js +++ b/addons/web/static/src/js/boot.js @@ -1,10 +1,7 @@ -/*--------------------------------------------------------- - * OpenERP Web Boostrap Code - *---------------------------------------------------------*/ - /** - * @name openerp - * @namespace openerp + *------------------------------------------------------------------------------ + * Odoo Web Boostrap Code + *------------------------------------------------------------------------------ * * Each module can return a promise. In that case, the module is marked as loaded * only when the promise is resolved, and its value is equal to the resolved value. @@ -26,274 +23,317 @@ * Debug: * Non loaded or failed module informations for debugging */ - (function () { "use strict"; + var jobUID = Date.now(); + var jobs = []; var factories = Object.create(null); - var job_names = []; - var job_deps = []; - var job_promise = []; - + var jobDeps = []; + var jobPromises = []; var services = Object.create({}); var commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg; var cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g; - var debug = ($.deparam($.param.querystring()).debug !== undefined); + var debug = /\bdebug\b/.test(window.location.search); + + if (!window.odoo) { + window.odoo = {}; + } + var odoo = window.odoo; - var odoo = window.odoo = window.odoo || {}; var didLogInfoResolve; var didLogInfoPromise = new Promise(function (resolve) { didLogInfoResolve = resolve; }); - _.extend(odoo, { - testing: typeof QUnit === "object", - debug: debug, - remaining_jobs: jobs, - - __DEBUG__: { - didLogInfo: didLogInfoPromise, - get_dependencies: function (name, transitive) { - var deps = name instanceof Array ? name : [name], - changed; - do { - changed = false; - _.each(job_deps, function (dep) { - if (_.contains(deps, dep.to) && (!_.contains(deps, dep.from))) { - deps.push(dep.from); - changed = true; - } - }); - } while (changed && transitive); - return deps; - }, - get_dependents: function (name) { - return _.pluck(_.where(job_deps, { from: name }), 'to'); - }, - get_waited_jobs: function () { - return _.uniq(_.map(jobs, function (job) { return job.name; })); - }, - get_missing_jobs: function () { - var self = this; - var waited = this.get_waited_jobs(); - var missing = []; - _.each(waited, function (job) { - _.each(self.get_dependencies(job), function (job) { - if (!(job in self.services)) { - missing.push(job); - } - }); + odoo.testing = typeof QUnit === 'object'; + odoo.debug = debug; + odoo.remainingJobs = jobs; + odoo.__DEBUG__ = { + didLogInfo: didLogInfoPromise, + getDependencies: function (name, transitive) { + var deps = name instanceof Array ? name : [name]; + var changed; + do { + changed = false; + jobDeps.forEach(function (dep) { + if (deps.indexOf(dep.to) >= 0 && deps.indexOf(dep.from) < 0) { + deps.push(dep.from); + changed = true; + } }); - return _.filter(_.difference(_.uniq(missing), waited), function (job) { return !job.error; }); - }, - get_failed_jobs: function () { - return _.filter(jobs, function (job) { return !!job.error; }); - }, - factories: factories, - services: services, + } while (changed && transitive); + return deps; }, - define: function () { - var args = Array.prototype.slice.call(arguments); - var name = typeof args[0] === 'string' ? args.shift() : _.uniqueId('__job'); - var factory = args[args.length - 1]; - var deps; - if (args[0] instanceof Array) { - deps = args[0]; - } else { - deps = []; - factory.toString() - .replace(commentRegExp, '') - .replace(cjsRequireRegExp, function (match, dep) { - deps.push(dep); - }); - } - - if (odoo.debug) { - if (!(deps instanceof Array)) { - throw new Error('Dependencies should be defined by an array', deps); - } - if (typeof factory !== 'function') { - throw new Error('Factory should be defined by a function', factory); - } - if (typeof name !== 'string') { - throw new Error("Invalid name definition (should be a string", name); - } - if (name in factories) { - throw new Error("Service " + name + " already defined"); - } - } - - factory.deps = deps; - factories[name] = factory; - - jobs.push({ - name: name, - factory: factory, - deps: deps, + getDependents: function (name) { + return jobDeps.filter(function (dep) { + return dep.from === name; + }).map(function (dep) { + return dep.to; }); - - job_names.push(name); - _.each(deps, function (dep) { - job_deps.push({ from: dep, to: name }); - }); - - this.process_jobs(jobs, services); }, - log: function () { + getWaitedJobs: function () { + return jobs.map(function (job) { + return job.name; + }).filter(function (item, index, self) { // uniq + return self.indexOf(item) === index; + }); + }, + getMissingJobs: function () { + var self = this; + var waited = this.getWaitedJobs(); var missing = []; - var failed = []; - - if (jobs.length) { - var debug_jobs = {}; - var rejected = []; - var rejected_linked = []; - var job; - var jobdep; - - for (var k = 0; k < jobs.length; k++) { - debug_jobs[jobs[k].name] = job = { - dependencies: jobs[k].deps, - dependents: odoo.__DEBUG__.get_dependents(jobs[k].name), - name: jobs[k].name - }; - if (jobs[k].error) { - job.error = jobs[k].error; + waited.forEach(function (job) { + self.getDependencies(job).forEach(function (job) { + if (!(job in self.services)) { + missing.push(job); } - if (jobs[k].rejected) { - job.rejected = jobs[k].rejected; - rejected.push(job.name); - } - var deps = odoo.__DEBUG__.get_dependencies(job.name); - for (var i = 0; i < deps.length; i++) { - if (job.name !== deps[i] && !(deps[i] in services)) { - jobdep = debug_jobs[deps[i]] || (deps[i] in factories && _.find(jobs, function (job) { return job.name === deps[i]; })); - if (jobdep && jobdep.rejected) { - if (!job.rejected) { - job.rejected = []; - rejected_linked.push(job.name); + }); + }); + return missing.filter(function (item, index, self) { + return self.indexOf(item) === index; + }).filter(function (item) { + return waited.indexOf(item) < 0; + }).filter(function (job) { + return !job.error; + }); + }, + getFailedJobs: function () { + return jobs.filter(function (job) { + return !!job.error; + }); + }, + factories: factories, + services: services, + }; + odoo.define = function () { + var args = Array.prototype.slice.call(arguments); + var name = typeof args[0] === 'string' ? args.shift() : ('__odoo_job' + (jobUID++)); + var factory = args[args.length - 1]; + var deps; + if (args[0] instanceof Array) { + deps = args[0]; + } else { + deps = []; + factory.toString() + .replace(commentRegExp, '') + .replace(cjsRequireRegExp, function (match, dep) { + deps.push(dep); + }); + } + + if (odoo.debug) { + if (!(deps instanceof Array)) { + throw new Error('Dependencies should be defined by an array', deps); + } + if (typeof factory !== 'function') { + throw new Error('Factory should be defined by a function', factory); + } + if (typeof name !== 'string') { + throw new Error("Invalid name definition (should be a string", name); + } + if (name in factories) { + throw new Error("Service " + name + " already defined"); + } + } + + factory.deps = deps; + factories[name] = factory; + + jobs.push({ + name: name, + factory: factory, + deps: deps, + }); + + deps.forEach(function (dep) { + jobDeps.push({from: dep, to: name}); + }); + + this.processJobs(jobs, services); + }; + odoo.log = function () { + var missing = []; + var failed = []; + + if (jobs.length) { + var debugJobs = {}; + var rejected = []; + var rejectedLinked = []; + var job; + var jobdep; + + for (var k = 0; k < jobs.length; k++) { + debugJobs[jobs[k].name] = job = { + dependencies: jobs[k].deps, + dependents: odoo.__DEBUG__.getDependents(jobs[k].name), + name: jobs[k].name + }; + if (jobs[k].error) { + job.error = jobs[k].error; + } + if (jobs[k].rejected) { + job.rejected = jobs[k].rejected; + rejected.push(job.name); + } + var deps = odoo.__DEBUG__.getDependencies(job.name); + for (var i = 0; i < deps.length; i++) { + if (job.name !== deps[i] && !(deps[i] in services)) { + jobdep = debugJobs[deps[i]]; + if (!jobdep && deps[i] in factories) { + for (var j = 0; j < jobs.length; j++) { + if (jobs[j].name === deps[i]) { + jobdep = jobs[j]; + break; } - job.rejected.push(deps[i]); - } else { - if (!job.missing) { - job.missing = []; - } - job.missing.push(deps[i]); } } + if (jobdep && jobdep.rejected) { + if (!job.rejected) { + job.rejected = []; + rejectedLinked.push(job.name); + } + job.rejected.push(deps[i]); + } else { + if (!job.missing) { + job.missing = []; + } + job.missing.push(deps[i]); + } } } - missing = odoo.__DEBUG__.get_missing_jobs(); - failed = odoo.__DEBUG__.get_failed_jobs(); - var unloaded = _.filter(debug_jobs, function (job) { return job.missing; }); - - if (odoo.debug || !_.isEmpty(failed) || !_.isEmpty(unloaded)) { - var log = console[_.isEmpty(failed) || _.isEmpty(unloaded) ? 'info' : 'error'].bind(console); - log((_.isEmpty(failed) ? (_.isEmpty(unloaded) ? 'info' : 'warning') : 'error') + ': Some modules could not be started'); - if (missing.length) { - log('Missing dependencies: ', missing); - } - if (!_.isEmpty(failed)) { - log('Failed modules: ', _.pluck(failed, 'name')); - } - if (!_.isEmpty(rejected)) { - log('Rejected modules: ', rejected); - } - if (!_.isEmpty(rejected_linked)) { - log('Rejected linked modules: ', rejected_linked); - } - if (!_.isEmpty(unloaded)) { - log('Non loaded modules: ', _.pluck(unloaded, 'name')); - } - if (odoo.debug && !_.isEmpty(debug_jobs)) { - log('Debug: ', debug_jobs); - } - } } - odoo.__DEBUG__.js_modules = { - missing: missing, - failed: _.pluck(failed, 'name'), - }; - didLogInfoResolve(); - }, - process_jobs: function (jobs, services) { - var job; - - function process_job(job) { - var require = make_require(job); - - var job_exec; - var def = new Promise(function (resolve) { - try { - job_exec = job.factory.call(null, require); - jobs.splice(jobs.indexOf(job), 1); - } catch (e) { - job.error = e; - console.error('Error while loading ' + job.name); - console.error(e.stack); - } - if (!job.error) { - Promise.resolve(job_exec).then( - function (data) { - services[job.name] = data; - resolve(); - odoo.process_jobs(jobs, services); - }).guardedCatch(function (e) { - job.rejected = e || true; - jobs.push(job); - resolve(); - } - ); - } + missing = odoo.__DEBUG__.getMissingJobs(); + failed = odoo.__DEBUG__.getFailedJobs(); + var unloaded = Object.keys(debugJobs) // Object.values is not supported + .map(function (key) { + return debugJobs[key]; + }).filter(function (job) { + return job.missing; }); - job_promise.push(def); - } - function is_ready(job) { - return !job.error && !job.rejected && _.every(job.factory.deps, function (name) { return name in services; }); - } - - function make_require(job) { - var deps = _.pick(services, job.deps); - - function require(name) { - if (!(name in deps)) { - console.error('Undefined dependency: ', name); - } else { - require.__require_calls++; - } - return deps[name]; + if (odoo.debug || failed.length || unloaded.length) { + var log = window.console[!failed.length || !unloaded.length ? 'info' : 'error'].bind(window.console); + log((failed.length ? 'error' : (unloaded.length ? 'warning' : 'info')) + ': Some modules could not be started'); + if (missing.length) { + log('Missing dependencies: ', missing); + } + if (failed.length) { + log('Failed modules: ', failed.map(function (fail) { + return fail.name; + })); + } + if (rejected.length) { + log('Rejected modules: ', rejected); + } + if (rejectedLinked.length) { + log('Rejected linked modules: ', rejectedLinked); + } + if (unloaded.length) { + log('Non loaded modules: ', unloaded.map(function (unload) { + return unload.name; + })); + } + if (odoo.debug && Object.keys(debugJobs).length) { + log('Debug: ', debugJobs); } - - require.__require_calls = 0; - return require; } - - while (jobs.length && (job = _.find(jobs, is_ready))) { - process_job(job); - } - - return services; } - }); + odoo.__DEBUG__.jsModules = { + missing: missing, + failed: failed.map(function (fail) { + return fail.name; + }), + }; + didLogInfoResolve(); + }; + odoo.processJobs = function (jobs, services) { + var job; + function processJob(job) { + var require = makeRequire(job); - // automatically log errors detected when loading modules - var log_when_loaded = function () { - _.delay(function () { - var len = job_promise.length; - Promise.all(job_promise).then(function () { - if (len === job_promise.length) { + var jobExec; + var def = new Promise(function (resolve) { + try { + jobExec = job.factory.call(null, require); + jobs.splice(jobs.indexOf(job), 1); + } catch (e) { + job.error = e; + console.error('Error while loading ' + job.name); + console.error(e.stack); + } + if (!job.error) { + Promise.resolve(jobExec).then( + function (data) { + services[job.name] = data; + resolve(); + odoo.processJobs(jobs, services); + }).guardedCatch(function (e) { + job.rejected = e || true; + jobs.push(job); + resolve(); + } + ); + } + }); + jobPromises.push(def); + } + + function isReady(job) { + return !job.error && !job.rejected && job.factory.deps.every(function (name) { + return name in services; + }); + } + + function makeRequire(job) { + var deps = {}; + Object.keys(services).filter(function (item) { + return job.deps.indexOf(item) >= 0; + }).forEach(function (key) { + deps[key] = services[key]; + }); + + return function require(name) { + if (!(name in deps)) { + console.error('Undefined dependency: ', name); + } + return deps[name]; + }; + } + + while (jobs.length) { + job = undefined; + for (var i = 0; i < jobs.length; i++) { + if (isReady(jobs[i])) { + job = jobs[i]; + break; + } + } + if (!job) { + break; + } + processJob(job); + } + + return services; + }; + + // Automatically log errors detected when loading modules + window.addEventListener('load', function logWhenLoaded() { + setTimeout(function () { + var len = jobPromises.length; + Promise.all(jobPromises).then(function () { + if (len === jobPromises.length) { odoo.log(); } else { - log_when_loaded(); + logWhenLoaded(); } }); - }, 6000); - }; - $(log_when_loaded); - + }, 9999); + }); })(); diff --git a/addons/web/static/tests/boot_tests.js b/addons/web/static/tests/boot_tests.js index f317e77d2e5..7103c1dd5f9 100644 --- a/addons/web/static/tests/boot_tests.js +++ b/addons/web/static/tests/boot_tests.js @@ -1,13 +1,12 @@ -(function() { +(function () { "use strict"; -odoo.__DEBUG__.didLogInfo.then(function() { - - var modulesInfo = odoo.__DEBUG__.js_modules; +odoo.__DEBUG__.didLogInfo.then(function () { + var modulesInfo = odoo.__DEBUG__.jsModules; QUnit.module('Odoo JS Modules'); - QUnit.test('all modules are properly loaded', function(assert) { + QUnit.test('all modules are properly loaded', function (assert) { assert.expect(2); assert.deepEqual(modulesInfo.missing, [], @@ -15,7 +14,5 @@ odoo.__DEBUG__.didLogInfo.then(function() { assert.deepEqual(modulesInfo.failed, [], "no js module should have failed"); }); - }); - -})(); \ No newline at end of file +})();