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>
This commit is contained in:
RossAscends
2025-05-26 07:38:32 +09:00
committed by GitHub
parent e12e0ccd84
commit 57882c80e5

View File

@@ -36,7 +36,13 @@ export let modules = [];
* A set of active extensions.
* @type {Set<string>}
*/
let activeExtensions = new Set();
const activeExtensions = new Set();
/**
* Errors that occurred while loading extensions.
* @type {Set<string>}
*/
const extensionLoadErrors = new Set();
const getApiUrl = () => extension_settings.apiUrl;
const 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) {
*/
async function activateExtensions() {
const extensions = Object.entries(manifests).sort((a, b) => sortManifestsByOrder(a[1], b[1]));
const extensionNames = extensions.map(x => x[0]);
const promises = [];
for (let entry of extensions) {
const name = entry[0];
const manifest = entry[1];
const extrasRequirements = manifest.requires;
const extensionDependencies = manifest.dependencies;
const displayName = manifest.display_name || name;
if (activeExtensions.has(name)) {
continue;
}
const meetsModuleRequirements = !Array.isArray(manifest.requires) || isSubsetOf(modules, manifest.requires);
// Module requirements: pass if 'requires' is undefined, null, or not an array; check subset if it's an array
let meetsModuleRequirements = true;
let missingModules = [];
if (extrasRequirements !== undefined) {
if (Array.isArray(extrasRequirements)) {
meetsModuleRequirements = isSubsetOf(modules, extrasRequirements);
missingModules = extrasRequirements.filter(req => !modules.includes(req));
} else {
console.warn(`Extension ${name}: manifest.json 'requires' field is not an array. Loading allowed, but any intended requirements were not verified to exist.`);
}
}
// Extension dependencies: pass if 'dependencies' is undefined or not an array; check subset and disabled status if it's an array
let meetsExtensionDeps = true;
let missingDependencies = [];
let disabledDependencies = [];
if (extensionDependencies !== undefined) {
if (Array.isArray(extensionDependencies)) {
// Check if all dependencies exist
meetsExtensionDeps = isSubsetOf(extensionNames, extensionDependencies);
missingDependencies = extensionDependencies.filter(dep => !extensionNames.includes(dep));
// Check for disabled dependencies
if (meetsExtensionDeps) {
disabledDependencies = extensionDependencies.filter(dep => extension_settings.disabledExtensions.includes(dep));
if (disabledDependencies.length > 0) {
// Fail if any dependencies are disabled
meetsExtensionDeps = false;
}
}
} else {
console.warn(`Extension ${name}: manifest.json 'dependencies' field is not an array. Loading allowed, but any intended requirements were not verified to exist.`);
}
}
const isDisabled = extension_settings.disabledExtensions.includes(name);
if (meetsModuleRequirements && !isDisabled) {
if (meetsModuleRequirements && meetsExtensionDeps && !isDisabled) {
try {
console.debug('Activating extension', name);
const promise = addExtensionLocale(name, manifest).finally(() => Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)]));
const promise = addExtensionLocale(name, manifest).finally(() =>
Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)]),
);
await promise
.then(() => activeExtensions.add(name))
.catch(err => console.log('Could not activate extension', name, err));
.catch(err => {
console.log('Could not activate extension', name, err);
extensionLoadErrors.add(t`Extension "${displayName}" failed to load: ${err}`);
});
promises.push(promise);
} catch (error) {
console.error('Could not activate extension', name, error);
}
catch (error) {
console.error('Could not activate extension', name);
console.error(error);
} else if (!meetsModuleRequirements && !isDisabled) {
console.warn(t`Extension "${name}" did not load. Missing required Extras module(s): "${missingModules.join(', ')}"`);
extensionLoadErrors.add(t`Extension "${displayName}" did not load. Missing required Extras module(s): "${missingModules.join(', ')}"`);
} else if (!meetsExtensionDeps && !isDisabled) {
if (disabledDependencies.length > 0) {
console.warn(t`Extension "${name}" did not load. Required extensions exist but are disabled: "${disabledDependencies.join(', ')}". Enable them first, then reload.`);
extensionLoadErrors.add(t`Extension "${displayName}" did not load. Required extensions exist but are disabled: "${disabledDependencies.join(', ')}". Enable them first, then reload.`);
} else {
console.warn(t`Extension "${name}" did not load. Missing required extensions: "${missingDependencies.join(', ')}"`);
extensionLoadErrors.add(t`Extension "${displayName}" did not load. Missing required extensions: "${missingDependencies.join(', ')}"`);
}
}
}
await Promise.allSettled(promises);
$('#extensions_details').toggleClass('warning', extensionLoadErrors.size > 0);
}
async function connectClickHandler() {
@@ -751,6 +809,27 @@ function getModuleInformation() {
`;
}
/**
* Generates HTML for the extension load errors.
* @returns {string} HTML string containing the errors that occurred while loading extensions.
*/
function getExtensionLoadErrorsHtml() {
if (extensionLoadErrors.size === 0) {
return '';
}
const container = document.createElement('div');
container.classList.add('info-block', 'error');
for (const error of extensionLoadErrors) {
const errorElement = document.createElement('div');
errorElement.textContent = error;
container.appendChild(errorElement);
}
return container.outerHTML;
}
/**
* Generates the HTML strings for all extensions and displays them in a popup.
*/
@@ -765,6 +844,7 @@ async function showExtensionsDetails() {
initialScrollTop = oldPopup.content.scrollTop;
await oldPopup.completeCancelled();
}
const htmlErrors = getExtensionLoadErrorsHtml();
const htmlDefault = $('<div class="marginBot10"><h3 class="textAlignCenter">' + t`Built-in Extensions:` + '</h3></div>');
const htmlExternal = $('<div class="marginBot10"><h3 class="textAlignCenter">' + t`Installed Extensions:` + '</h3></div>');
const htmlLoading = $(`<div class="flex-container alignItemsCenter justifyCenter marginTop10 marginBot5">
@@ -787,6 +867,7 @@ async function showExtensionsDetails() {
const html = $('<div></div>')
.addClass('extensions_info')
.append(htmlErrors)
.append(htmlDefault)
.append(htmlExternal)
.append(getModuleInformation());