/** *------------------------------------------------------------------------------ * 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. * The module can be rejected (unloaded). This will be logged in the console as info. * * logs: * Missing dependencies: * These modules do not appear in the page. It is possible that the * JavaScript file is not in the page or that the module name is wrong * Failed modules: * A javascript error is detected * Rejected modules: * The module returns a rejected promise. It (and its dependent modules) * is not loaded. * Rejected linked modules: * Modules who depend on a rejected module * Non loaded modules: * Modules who depend on a missing or a failed module * Debug: * Non loaded or failed module informations for debugging */ (function () { "use strict"; var jobUID = Date.now(); var jobs = []; var factories = Object.create(null); var jobDeps = []; var jobPromises = []; var services = Object.create({}); var commentRegExp = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/gm; var cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g; if (!globalThis.odoo) { globalThis.odoo = {}; } var odoo = globalThis.odoo; var debug = odoo.debug; var didLogInfoResolve; var didLogInfoPromise = new Promise(function (resolve) { didLogInfoResolve = resolve; }); 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; } }); } while (changed && transitive); return deps; }, getDependents: function (name) { return jobDeps .filter(function (dep) { return dep.from === name; }) .map(function (dep) { return dep.to; }); }, 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 = []; waited.forEach(function (job) { self.getDependencies(job).forEach(function (job) { if (!(job in self.services)) { missing.push(job); } }); }); 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; }); }, processJobs: function () { var job; function processJob(job) { var require = makeRequire(job); var jobExec; function onError(e) { job.error = e; console.error(`Error while loading ${job.name}: ${e.message}`, e); Promise.reject(e); } var def = new Promise(function (resolve) { try { jobExec = job.factory.call(null, require); jobs.splice(jobs.indexOf(job), 1); } catch (e) { onError(e); } if (!job.error) { Promise.resolve(jobExec) .then(function (data) { services[job.name] = data; resolve(); odoo.__DEBUG__.processJobs(); }) .guardedCatch(function (e) { job.rejected = e || true; jobs.push(job); }) .catch(function (e) { if (e instanceof Error) { onError(e); } resolve(); }); } else { resolve(); } }); jobPromises.push(def); def.then(job.resolve); } 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; }, 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 (!(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; let promiseResolve; const promise = new Promise((resolve) => { promiseResolve = resolve; }); jobs.push({ name: name, factory: factory, deps: deps, resolve: promiseResolve, promise: promise, }); deps.forEach(function (dep) { jobDeps.push({ from: dep, to: name }); }); odoo.__DEBUG__.processJobs(); }; odoo.log = function () { var missing = []; var failed = []; var cycle = null; 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; } } } 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__.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; }); if (debug || failed.length || unloaded.length) { var log = globalThis.console[ !failed.length || !unloaded.length ? "info" : "error" ].bind(globalThis.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) { cycle = findCycle(unloaded); if (cycle) { console.error("Cyclic dependencies: " + cycle); } log( "Non loaded modules: ", unloaded.map(function (unload) { return unload.name; }) ); } if (debug && Object.keys(debugJobs).length) { log("Debug: ", debugJobs); } } } odoo.__DEBUG__.jsModules = { missing: missing, failed: failed.map((mod) => mod.name), unloaded: unloaded ? unloaded.map((mod) => mod.name) : [], cycle, }; didLogInfoResolve(true); }; /** * Returns a resolved promise when the targeted services are loaded. * If no service is found the promise is used directly. * * @param {string|RegExp} serviceName name of the service to expect * or regular expression matching the service. * @returns {Promise} resolved when the services ares * loaded. The value is equal to the number of services found. */ odoo.ready = async function (serviceName) { function match(name) { return typeof serviceName === "string" ? name === serviceName : serviceName.test(name); } await Promise.all(jobs.filter((job) => match(job.name)).map((job) => job.promise)); return Object.keys(factories).filter(match).length; }; odoo.runtimeImport = function (moduleName) { if (!(moduleName in services)) { throw new Error(`Service "${moduleName} is not defined or isn't finished loading."`); } return services[moduleName]; }; // Automatically log errors detected when loading modules globalThis.addEventListener("load", function logWhenLoaded() { const len = jobPromises.length; Promise.all(jobPromises).then(function () { if (len === jobPromises.length) { odoo.log(); } else { logWhenLoaded(); } }); }); /** * Visit the list of jobs, and return the first found cycle, if any * * @param {any[]} jobs * @returns {null | string} either a string describing a cycle, or null */ function findCycle(jobs) { // build dependency graph const dependencyGraph = new Map(); for (const job of jobs) { dependencyGraph.set(job.name, job.dependencies); } // helpers function visitJobs(jobs, visited = new Set()) { for (const job of jobs) { const result = visitJob(job, visited); if (result) { return result; } } return null; } function visitJob(job, visited) { if (visited.has(job)) { const jobs = Array.from(visited).concat([job]); const index = jobs.indexOf(job); return jobs .slice(index) .map((j) => `"${j}"`) .join(" => "); } const deps = dependencyGraph.get(job); return deps ? visitJobs(deps, new Set(visited).add(job)) : null; } // visit each root to find cycles return visitJobs(jobs.map((j) => j.name)); } })();