[IMP] web: report module loading errors in the DOM

Previously, if there were module loading errors, they would typically
happen before the error service was ready, and so there is almost always
no user-facing error that shows up even though the application state may
be severely corrupted. This can be very confusing for developers who are
not used to working with JS and might not check the devtools console.

This commit makes it so that the module system will replace the contents
of the body with an error when some modules were unable to be loaded,
with the list of modules and the reason, so that the developer is not
confused as to why things aren't working as expected.

closes odoo/odoo#104155

Signed-off-by: Samuel Degueldre <sad@odoo.com>
This commit is contained in:
Samuel Degueldre
2022-10-27 15:41:15 +02:00
parent 84ef92fef7
commit c989ff339d
4 changed files with 104 additions and 37 deletions
+82 -36
View File
@@ -32,6 +32,7 @@
var factories = Object.create(null);
var jobDeps = [];
var jobPromises = [];
const failed = [];
var services = Object.create({});
@@ -75,41 +76,23 @@
});
},
getWaitedJobs: function () {
return jobs
.map(function (job) {
return job.name;
})
.filter(function (item, index, self) {
// uniq
return self.indexOf(item) === index;
});
return new Set(jobs.map((job) => job.name));
},
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);
getMissingJobs() {
const waited = this.getWaitedJobs();
const missing = new Set();
for (const job of waited) {
for (const dep of this.getDependencies(job)) {
if (
!(dep in this.services) &&
!waited.has(dep) &&
!failed.find((job) => job.name === dep)
) {
missing.add(dep);
}
});
});
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;
});
}
}
return [...missing];
},
processJobs: function () {
var job;
@@ -120,6 +103,7 @@
var jobExec;
function onError(e) {
job.error = e;
failed.push(job);
console.error(`Error while loading ${job.name}: ${e.message}`, e);
}
var def = new Promise(function (resolve) {
@@ -254,7 +238,6 @@
};
odoo.log = function () {
var missing = [];
var failed = [];
var cycle = null;
if (jobs.length) {
@@ -305,7 +288,6 @@
}
}
missing = odoo.__DEBUG__.getMissingJobs();
failed = odoo.__DEBUG__.getFailedJobs();
var unloaded = Object.keys(debugJobs) // Object.values is not supported
.map(function (key) {
return debugJobs[key];
@@ -356,12 +338,20 @@
}
}
}
odoo.__DEBUG__.jsModules = {
const moduleInfo = {
missing: missing,
failed: failed.map((mod) => mod.name),
unloaded: unloaded ? unloaded.map((mod) => mod.name) : [],
cycle,
};
odoo.__DEBUG__.jsModules = moduleInfo;
// The assets_tests bundle contains a lot of module whose dependencies are in other bundles
// that may or may not be loaded. This global is used to silence module loading errors when
// this bundle is present
if (!window.__odooIgnoreModuleErrors) {
displayModuleErrors(moduleInfo);
}
didLogInfoResolve(true);
};
/**
@@ -440,4 +430,60 @@
// visit each root to find cycles
return visitJobs(jobs.map((j) => j.name));
}
function displayModuleErrors({ failed, missing, unloaded, cycle }) {
const list = (heading, arr) => {
const frag = document.createDocumentFragment();
if (!arr || !arr.length) {
return frag;
}
frag.textContent = heading;
const ul = document.createElement("ul");
for (const el of arr) {
const li = document.createElement("li");
li.textContent = el;
ul.append(li);
}
frag.appendChild(ul);
return frag;
};
if ([failed, missing, unloaded].some((arr) => arr.length) || cycle) {
// Empty body
while (document.body.childNodes.length) {
document.body.childNodes[0].remove();
}
const container = document.createElement("div");
container.className =
"position-fixed w-100 h-100 d-flex align-items-center flex-column bg-white overflow-auto";
container.style.zIndex = "10000";
const alert = document.createElement("div");
alert.className = "alert alert-danger o_error_detail fw-bold m-auto";
container.appendChild(alert);
alert.appendChild(
list(
"The following modules failed to load because of an error, you may find more information in the devtools console:",
failed
)
);
alert.appendChild(
list(
"The following modules could not be loaded because they form a dependency cycle:",
cycle && [cycle]
)
);
alert.appendChild(
list(
"The following modules are needed by other modules but have not been defined, they may not be present in the correct asset bundle:",
missing
)
);
alert.appendChild(
list(
"The following modules could not be loaded because they have unmet dependencies, this is a secondary error which is likely caused by one of the above problems:",
unloaded
)
);
document.body.appendChild(container);
}
}
})();
+12
View File
@@ -20,6 +20,11 @@
<script type="text/javascript">
window.odoo = {};
window.odoo.__session_info__ = {is_report: true};
// Using web.assets_common without assets_frontend or assets_backend creates a lot
// of module dependency errors. Either assets_common should be reworked to work
// as a standalone bundle, or this template should not use it and add what it needs
// from it into its own bundle. Silence these errors in the meantime
window.__odooIgnoreModuleErrors = true;
</script>
<t t-call-assets="web.assets_common" t-css="false"/>
<t t-call-assets="web.report_assets_common" t-css="false"/>
@@ -46,6 +51,13 @@
<meta charset="utf-8"/>
<meta name="viewport" content="initial-scale=1"/>
<title><t t-esc="title or 'Odoo Report'"/></title>
<script type="text/javascript">
// Using web.assets_common without assets_frontend or assets_backend creates a lot
// of module dependency errors. Either assets_common should be reworked to work
// as a standalone bundle, or this template should not use it and add what it needs
// from it into its own bundle. Silence these errors in the meantime
window.__odooIgnoreModuleErrors = true;
</script>
<t t-call-assets="web.report_assets_common" t-js="false"/>
<t t-call-assets="web.assets_common" t-css="false"/>
<style>
+5 -1
View File
@@ -3,7 +3,11 @@
<!-- Call this template instead of "web.assets_tests" to have the proper conditional check -->
<template id="conditional_assets_tests" name="Tests Assets Bundle">
<t t-call-assets="web.assets_tests" t-if="'tests' in debug or test_mode_enabled" defer_load="True" />
<t t-if="'tests' in debug or test_mode_enabled">
<t t-call-assets="web.assets_tests" defer_load="True" />
<!-- The assets_tests bundle contains a lot of module whose dependencies are in other bundles that may or may not be loaded. This global is used to silence module loading errors when this bundle is present -->
<script>window.__odooIgnoreModuleErrors = true</script>
</t>
</template>
<template id="web.layout" name="Web layout">&lt;!DOCTYPE html&gt;
@@ -11,6 +11,11 @@
<title><t t-esc="slide.name"/></title>
<t t-call-assets="web.assets_common" t-js="false"/>
<t t-call-assets="website_slides.slide_embed_assets" t-js="false"/>
<!-- Using web.assets_common without assets_frontend or assets_backend creates a lot
of module dependency errors. Either assets_common should be reworked to work
as a standalone bundle, or this template should not use it and add what it needs
from it into its own bundle. Silence these errors in the meantime -->
<script>window.__odooIgnoreModuleErrors = true</script>
<t t-call-assets="web.assets_common" t-css="false"/>
<t t-call-assets="website_slides.slide_embed_assets" t-css="false"/>
</head>