| 380 | 386 | */ |
| 381 | 387 | async function activateExtensions() { |
| 382 | 388 | const extensions = Object.entries(manifests).sort((a, b) => sortManifestsByOrder(a[1], b[1])); |
| 389 | + const extensionNames = extensions.map(x => x[0]); |
| 383 | 390 | const promises = []; |
| 384 | 391 | |
| 385 | 392 | for (let entry of extensions) { |
| 386 | 393 | const name = entry[0]; |
| 387 | 394 | const manifest = entry[1]; |
| 395 | + const extrasRequirements = manifest.requires; |
| 396 | + const extensionDependencies = manifest.dependencies; |
| 397 | + const displayName = manifest.display_name || name; |
| 388 | 398 | |
| 389 | 399 | if (activeExtensions.has(name)) { |
| 390 | 400 | continue; |
| 391 | 401 | } |
| 392 | 402 | |
| 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 | 437 | const isDisabled = extension_settings.disabledExtensions.includes(name); |
| 395 | 438 | |
| 396 | 439 | if (meetsModuleRequirements && meetsExtensionDeps && !isDisabled) { |
| 397 | 440 | try { |
| 398 | 441 | console.debug('Activating extension', name); |
| 399 | 442 | const promise = addExtensionLocale(name, manifest).finally(() => Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)])); |
| 443 | + Promise.all([addExtensionScript(name, manifest), addExtensionStyle(name, manifest)]), |
| 444 | + ); |
| 400 | 445 | await promise |
| 401 | 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 | 451 | 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(', ')}"`); |
| 408 | 465 | } |
| 409 | 466 | } |
| 410 | 467 | } |
| 411 | 468 | |
| 412 | 469 | await Promise.allSettled(promises); |
| 470 | + $('#extensions_details').toggleClass('warning', extensionLoadErrors.size > 0); |
| 413 | 471 | } |
| 414 | 472 | |
| 415 | 473 | async function connectClickHandler() { |