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, +89 -8Ignore whitespace
public/scripts/extensions.js+89 -8
@@ -36,7 +36,13 @@ export let modules = [];
36 * A set of active extensions.36 * A set of active extensions.
37 * @type {Set<string>}37 * @type {Set<string>}
38 */38 */
39let activeExtensions = new Set();39const activeExtensions = new Set();
40
41/**
42 * Errors that occurred while loading extensions.
43 * @type {Set<string>}
44 */
45const extensionLoadErrors = new Set();
4046
41const getApiUrl = () => extension_settings.apiUrl;47const getApiUrl = () => extension_settings.apiUrl;
42const sortManifestsByOrder = (a, b) => parseInt(a.loading_order) - parseInt(b.loading_order) || String(a.display_name).localeCompare(String(b.display_name));48const 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) {
380 */386 */
381async function activateExtensions() {387async function activateExtensions() {
382 const extensions = Object.entries(manifests).sort((a, b) => sortManifestsByOrder(a[1], b[1]));388 const extensions = Object.entries(manifests).sort((a, b) => sortManifestsByOrder(a[1], b[1]));
389 const extensionNames = extensions.map(x => x[0]);
383 const promises = [];390 const promises = [];
384391
385 for (let entry of extensions) {392 for (let entry of extensions) {
386 const name = entry[0];393 const name = entry[0];
387 const manifest = entry[1];394 const manifest = entry[1];
395 const extrasRequirements = manifest.requires;
396 const extensionDependencies = manifest.dependencies;
397 const displayName = manifest.display_name || name;
388398
389 if (activeExtensions.has(name)) {399 if (activeExtensions.has(name)) {
390 continue;400 continue;
391 }401 }
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
394 const isDisabled = extension_settings.disabledExtensions.includes(name);437 const isDisabled = extension_settings.disabledExtensions.includes(name);
395438
396 if (meetsModuleRequirements && !isDisabled) {439 if (meetsModuleRequirements && meetsExtensionDeps && !isDisabled) {
397 try {440 try {
398 console.debug('Activating extension', name);441 console.debug('Activating extension', name);
399 const promise = addExtensionLocale(name, manifest).finally(() => Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)]));442 const promise = addExtensionLocale(name, manifest).finally(() =>
443 Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)]),
444 );
400 await promise445 await promise
401 .then(() => activeExtensions.add(name))446 .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 });
403 promises.push(promise);451 promises.push(promise);
452 } catch (error) {
453 console.error('Could not activate extension', name, error);
404 }454 }
405 catch (error) {455 } else if (!meetsModuleRequirements && !isDisabled) {
406 console.error('Could not activate extension', name);456 console.warn(t`Extension "${name}" did not load. Missing required Extras module(s): "${missingModules.join(', ')}"`);
407 console.error(error);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(', ')}"`);
408 }465 }
409 }466 }
410 }467 }
411468
412 await Promise.allSettled(promises);469 await Promise.allSettled(promises);
470 $('#extensions_details').toggleClass('warning', extensionLoadErrors.size > 0);
413}471}
414472
415async function connectClickHandler() {473async function connectClickHandler() {
@@ -752,6 +810,27 @@ function getModuleInformation() {
752}810}
753811
754/**812/**
813 * Generates HTML for the extension load errors.
814 * @returns {string} HTML string containing the errors that occurred while loading extensions.
815 */
816function 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/**
755 * Generates the HTML strings for all extensions and displays them in a popup.834 * Generates the HTML strings for all extensions and displays them in a popup.
756 */835 */
757async function showExtensionsDetails() {836async function showExtensionsDetails() {
@@ -765,6 +844,7 @@ async function showExtensionsDetails() {
765 initialScrollTop = oldPopup.content.scrollTop;844 initialScrollTop = oldPopup.content.scrollTop;
766 await oldPopup.completeCancelled();845 await oldPopup.completeCancelled();
767 }846 }
847 const htmlErrors = getExtensionLoadErrorsHtml();
768 const htmlDefault = $('<div class="marginBot10"><h3 class="textAlignCenter">' + t`Built-in Extensions:` + '</h3></div>');848 const htmlDefault = $('<div class="marginBot10"><h3 class="textAlignCenter">' + t`Built-in Extensions:` + '</h3></div>');
769 const htmlExternal = $('<div class="marginBot10"><h3 class="textAlignCenter">' + t`Installed Extensions:` + '</h3></div>');849 const htmlExternal = $('<div class="marginBot10"><h3 class="textAlignCenter">' + t`Installed Extensions:` + '</h3></div>');
770 const htmlLoading = $(`<div class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5">850 const htmlLoading = $(`<div class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5">
@@ -787,6 +867,7 @@ async function showExtensionsDetails() {
787867
788 const html = $('<div></div>')868 const html = $('<div></div>')
789 .addClass('extensions_info')869 .addClass('extensions_info')
870 .append(htmlErrors)
790 .append(htmlDefault)871 .append(htmlDefault)
791 .append(htmlExternal)872 .append(htmlExternal)
792 .append(getModuleInformation());873 .append(getModuleInformation());