add ability for exts to req other exts (#4023) * add ability for exts to req other exts * Get rid of toasts. Collect errors on load, display in manager * Remove unused variable * Only show missing modules/dependencies * Prefer display names in validation messages * Prefer internal name for console warn --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

57882c80e5078678488b1b4107a1b13c2bd7d0d0

RossAscends <124905043+RossAscends@users.noreply.github.com>

Signed
1 files changed, +90 -9Showing whitespace changes
public/scripts/extensions.js+90 -9
@@ -36,7 +36,13 @@ export let modules = [];
3636 * A set of active extensions.
3737 * @type {Set<string>}
3838 */
3939letconst activeExtensions = new Set();
40+
41+/**
42+ * Errors that occurred while loading extensions.
43+ * @type {Set<string>}
44+ */
45+const extensionLoadErrors = new Set();
4046
4147const getApiUrl = () => extension_settings.apiUrl;
4248const sortManifestsByOrder = (a, b) => parseInt(a.loading_order) - parseInt(b.loading_order) || String(a.display_name).localeCompare(String(b.display_name));
@@ -380,36 +386,88 @@ async function getManifests(names) {
380386 */
381387async function activateExtensions() {
382388 const extensions = Object.entries(manifests).sort((a, b) => sortManifestsByOrder(a[1], b[1]));
389+ const extensionNames = extensions.map(x => x[0]);
383390 const promises = [];
384391
385392 for (let entry of extensions) {
386393 const name = entry[0];
387394 const manifest = entry[1];
395+ const extrasRequirements = manifest.requires;
396+ const extensionDependencies = manifest.dependencies;
397+ const displayName = manifest.display_name || name;
388398
389399 if (activeExtensions.has(name)) {
390400 continue;
391401 }
392402
393- const meetsModuleRequirements = !Array.isArray(manifest.requires) || isSubsetOf(modules, manifest.requires);
403+ // Module requirements: pass if 'requires' is undefined, null, or not an array; check subset if it's an array
404+ let meetsModuleRequirements = true;
405+ let missingModules = [];
406+ if (extrasRequirements !== undefined) {
407+ if (Array.isArray(extrasRequirements)) {
408+ meetsModuleRequirements = isSubsetOf(modules, extrasRequirements);
409+ missingModules = extrasRequirements.filter(req => !modules.includes(req));
410+ } else {
411+ console.warn(`Extension ${name}: manifest.json 'requires' field is not an array. Loading allowed, but any intended requirements were not verified to exist.`);
412+ }
413+ }
414+
415+ // Extension dependencies: pass if 'dependencies' is undefined or not an array; check subset and disabled status if it's an array
416+ let meetsExtensionDeps = true;
417+ let missingDependencies = [];
418+ let disabledDependencies = [];
419+ if (extensionDependencies !== undefined) {
420+ if (Array.isArray(extensionDependencies)) {
421+ // Check if all dependencies exist
422+ meetsExtensionDeps = isSubsetOf(extensionNames, extensionDependencies);
423+ missingDependencies = extensionDependencies.filter(dep => !extensionNames.includes(dep));
424+ // Check for disabled dependencies
425+ if (meetsExtensionDeps) {
426+ disabledDependencies = extensionDependencies.filter(dep => extension_settings.disabledExtensions.includes(dep));
427+ if (disabledDependencies.length > 0) {
428+ // Fail if any dependencies are disabled
429+ meetsExtensionDeps = false;
430+ }
431+ }
432+ } else {
433+ console.warn(`Extension ${name}: manifest.json 'dependencies' field is not an array. Loading allowed, but any intended requirements were not verified to exist.`);
434+ }
435+ }
436+
394437 const isDisabled = extension_settings.disabledExtensions.includes(name);
395438
396439 if (meetsModuleRequirements && meetsExtensionDeps && !isDisabled) {
397440 try {
398441 console.debug('Activating extension', name);
399442 const promise = addExtensionLocale(name, manifest).finally(() => Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)]));
443+ Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)]),
444+ );
400445 await promise
401446 .then(() => activeExtensions.add(name))
402- .catch(err => console.log('Could not activate extension', name, err));
447+ .catch(err => {
448+ console.log('Could not activate extension', name, err);
449+ extensionLoadErrors.add(t`Extension "${displayName}" failed to load: ${err}`);
450+ });
403451 promises.push(promise);
404- }
452+ } catch (error) {
405- catch (error) {
453+ console.error('Could not activate extension', name, error);
406- console.error('Could not activate extension', name);
454+ }
407- console.error(error);
455+ } else if (!meetsModuleRequirements && !isDisabled) {
456+ console.warn(t`Extension "${name}" did not load. Missing required Extras module(s): "${missingModules.join(', ')}"`);
457+ extensionLoadErrors.add(t`Extension "${displayName}" did not load. Missing required Extras module(s): "${missingModules.join(', ')}"`);
458+ } else if (!meetsExtensionDeps && !isDisabled) {
459+ if (disabledDependencies.length > 0) {
460+ console.warn(t`Extension "${name}" did not load. Required extensions exist but are disabled: "${disabledDependencies.join(', ')}". Enable them first, then reload.`);
461+ extensionLoadErrors.add(t`Extension "${displayName}" did not load. Required extensions exist but are disabled: "${disabledDependencies.join(', ')}". Enable them first, then reload.`);
462+ } else {
463+ console.warn(t`Extension "${name}" did not load. Missing required extensions: "${missingDependencies.join(', ')}"`);
464+ extensionLoadErrors.add(t`Extension "${displayName}" did not load. Missing required extensions: "${missingDependencies.join(', ')}"`);
408465 }
409466 }
410467 }
411468
412469 await Promise.allSettled(promises);
470+ $('#extensions_details').toggleClass('warning', extensionLoadErrors.size > 0);
413471}
414472
415473async function connectClickHandler() {
@@ -752,6 +810,27 @@ function getModuleInformation() {
752810}
753811
754812/**
813+ * Generates HTML for the extension load errors.
814+ * @returns {string} HTML string containing the errors that occurred while loading extensions.
815+ */
816+function getExtensionLoadErrorsHtml() {
817+ if (extensionLoadErrors.size === 0) {
818+ return '';
819+ }
820+
821+ const container = document.createElement('div');
822+ container.classList.add('info-block', 'error');
823+
824+ for (const error of extensionLoadErrors) {
825+ const errorElement = document.createElement('div');
826+ errorElement.textContent = error;
827+ container.appendChild(errorElement);
828+ }
829+
830+ return container.outerHTML;
831+}
832+
833+/**
755834 * Generates the HTML strings for all extensions and displays them in a popup.
756835 */
757836async function showExtensionsDetails() {
@@ -765,6 +844,7 @@ async function showExtensionsDetails() {
765844 initialScrollTop = oldPopup.content.scrollTop;
766845 await oldPopup.completeCancelled();
767846 }
847+ const htmlErrors = getExtensionLoadErrorsHtml();
768848 const htmlDefault = $('<div class="marginBot10"><h3 class="textAlignCenter">' + t`Built-in Extensions:` + '</h3></div>');
769849 const htmlExternal = $('<div class="marginBot10"><h3 class="textAlignCenter">' + t`Installed Extensions:` + '</h3></div>');
770850 const htmlLoading = $(`<div class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5">
@@ -787,6 +867,7 @@ async function showExtensionsDetails() {
787867
788868 const html = $('<div></div>')
789869 .addClass('extensions_info')
870+ .append(htmlErrors)
790871 .append(htmlDefault)
791872 .append(htmlExternal)
792873 .append(getModuleInformation());