this repo has no description
at v1.0.1 6.4 kB view raw
1import { 2 ExtensionWebExports, 3 DetectedExtension, 4 ProcessedExtensions 5} from "@moonlight-mod/types"; 6import { readConfig } from "../config"; 7import Logger from "../util/logger"; 8import { getExtensions } from "../extension"; 9import { registerPatch, registerWebpackModule } from "../patch"; 10import calculateDependencies from "../util/dependency"; 11import { createEventEmitter } from "../util/event"; 12 13const logger = new Logger("core/extension/loader"); 14 15async function loadExt(ext: DetectedExtension) { 16 webPreload: { 17 if (ext.scripts.web != null) { 18 const source = 19 ext.scripts.web + "\n//# sourceURL=file:///" + ext.scripts.webPath; 20 const fn = new Function("require", "module", "exports", source); 21 22 const module = { id: ext.id, exports: {} }; 23 fn.apply(window, [ 24 () => { 25 logger.warn("Attempted to require() from web"); 26 }, 27 module, 28 module.exports 29 ]); 30 31 const exports: ExtensionWebExports = module.exports; 32 if (exports.patches != null) { 33 let idx = 0; 34 for (const patch of exports.patches) { 35 if (Array.isArray(patch.replace)) { 36 for (const replacement of patch.replace) { 37 const newPatch = Object.assign({}, patch, { 38 replace: replacement 39 }); 40 41 registerPatch({ ...newPatch, ext: ext.id, id: idx }); 42 idx++; 43 } 44 } else { 45 registerPatch({ ...patch, ext: ext.id, id: idx }); 46 idx++; 47 } 48 } 49 } 50 51 if (exports.webpackModules != null) { 52 for (const [name, wp] of Object.entries(exports.webpackModules)) { 53 registerWebpackModule({ ...wp, ext: ext.id, id: name }); 54 } 55 } 56 } 57 } 58 59 nodePreload: { 60 if (ext.scripts.nodePath != null) { 61 try { 62 const module = require(ext.scripts.nodePath); 63 moonlightNode.nativesCache[ext.id] = module; 64 } catch (e) { 65 logger.error(`Failed to load extension "${ext.id}"`, e); 66 } 67 } 68 } 69 70 injector: { 71 if (ext.scripts.hostPath != null) { 72 try { 73 require(ext.scripts.hostPath); 74 } catch (e) { 75 logger.error(`Failed to load extension "${ext.id}"`, e); 76 } 77 } 78 } 79} 80 81/* 82 This function resolves extensions and loads them, split into a few stages: 83 84 - Duplicate detection (removing multiple extensions with the same internal ID) 85 - Dependency resolution (creating a dependency graph & detecting circular dependencies) 86 - Failed dependency pruning 87 - Implicit dependency resolution (enabling extensions that are dependencies of other extensions) 88 - Loading all extensions 89 90 Instead of constructing an order from the dependency graph and loading 91 extensions synchronously, we load them in parallel asynchronously. Loading 92 extensions fires an event on completion, which allows us to await the loading 93 of another extension, resolving dependencies & load order effectively. 94*/ 95export async function loadExtensions( 96 exts: DetectedExtension[] 97): Promise<ProcessedExtensions> { 98 const items = exts 99 .map((ext) => { 100 return { 101 id: ext.id, 102 data: ext 103 }; 104 }) 105 .sort((a, b) => a.id.localeCompare(b.id)); 106 107 const [sorted, dependencyGraph] = calculateDependencies( 108 items, 109 110 function fetchDep(id) { 111 return exts.find((x) => x.id === id) ?? null; 112 }, 113 114 function getDeps(item) { 115 return item.data.manifest.dependencies ?? []; 116 }, 117 118 function getIncompatible(item) { 119 return item.data.manifest.incompatible ?? []; 120 } 121 ); 122 exts = sorted.map((x) => x.data); 123 124 logger.debug( 125 "Implicit dependency stage - extension list:", 126 exts.map((x) => x.id) 127 ); 128 const config = readConfig(); 129 const implicitlyEnabled: string[] = []; 130 131 function isEnabledInConfig(ext: DetectedExtension) { 132 if (implicitlyEnabled.includes(ext.id)) return true; 133 134 const entry = config.extensions[ext.id]; 135 if (entry == null) return false; 136 137 if (entry === true) return true; 138 if (typeof entry === "object" && entry.enabled === true) return true; 139 140 return false; 141 } 142 143 function validateDeps(ext: DetectedExtension) { 144 if (isEnabledInConfig(ext)) { 145 const deps = dependencyGraph.get(ext.id)!; 146 for (const dep of deps.values()) { 147 validateDeps(exts.find((e) => e.id === dep)!); 148 } 149 } else { 150 const dependsOnMe = Array.from(dependencyGraph.entries()).filter( 151 ([, v]) => v?.has(ext.id) 152 ); 153 154 if (dependsOnMe.length > 0) { 155 logger.debug("Implicitly enabling extension", ext.id); 156 implicitlyEnabled.push(ext.id); 157 } 158 } 159 } 160 161 for (const ext of exts) validateDeps(ext); 162 exts = exts.filter((e) => isEnabledInConfig(e)); 163 164 return { 165 extensions: exts, 166 dependencyGraph 167 }; 168} 169 170export async function loadProcessedExtensions({ 171 extensions, 172 dependencyGraph 173}: ProcessedExtensions) { 174 const eventEmitter = createEventEmitter(); 175 const finished: Set<string> = new Set(); 176 177 logger.debug( 178 "Load stage - extension list:", 179 extensions.map((x) => x.id) 180 ); 181 182 async function loadExtWithDependencies(ext: DetectedExtension) { 183 const deps = Array.from(dependencyGraph.get(ext.id)!); 184 185 // Wait for the dependencies to finish 186 const waitPromises = deps.map( 187 (dep: string) => 188 new Promise<void>((r) => { 189 function cb(eventDep: string) { 190 if (eventDep === dep) { 191 done(); 192 } 193 } 194 195 function done() { 196 eventEmitter.removeEventListener("ext-ready", cb); 197 r(); 198 } 199 200 eventEmitter.addEventListener("ext-ready", cb); 201 if (finished.has(dep)) done(); 202 }) 203 ); 204 205 if (waitPromises.length > 0) { 206 logger.debug( 207 `Waiting on ${waitPromises.length} dependencies for "${ext.id}"` 208 ); 209 await Promise.all(waitPromises); 210 } 211 212 logger.debug(`Loading "${ext.id}"`); 213 await loadExt(ext); 214 215 finished.add(ext.id); 216 eventEmitter.dispatchEvent("ext-ready", ext.id); 217 logger.debug(`Loaded "${ext.id}"`); 218 } 219 220 webPreload: { 221 for (const ext of extensions) { 222 moonlight.enabledExtensions.add(ext.id); 223 } 224 } 225 226 logger.debug("Loading all extensions"); 227 await Promise.all(extensions.map(loadExtWithDependencies)); 228 logger.info(`Loaded ${extensions.length} extensions`); 229}