| 380 | */ | 386 | */ |
| 381 | async function activateExtensions() { | 387 | async 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 = []; |
| 384 | | 391 | |
| 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; |
| 388 | | 398 | |
| 389 | if (activeExtensions.has(name)) { | 399 | if (activeExtensions.has(name)) { |
| 390 | continue; | 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 | const isDisabled = extension_settings.disabledExtensions.includes(name); | 437 | const isDisabled = extension_settings.disabledExtensions.includes(name); |
| 395 | | 438 | |
| 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 promise | 445 | 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 | } |
| 411 | | 468 | |
| 412 | await Promise.allSettled(promises); | 469 | await Promise.allSettled(promises); |
| | 470 | $('#extensions_details').toggleClass('warning', extensionLoadErrors.size > 0); |
| 413 | } | 471 | } |
| 414 | | 472 | |
| 415 | async function connectClickHandler() { | 473 | async function connectClickHandler() { |