this repo has no description
at v1.0.2 6.8 kB view raw
1import { 2 ExtensionWebExports, 3 DetectedExtension, 4 ProcessedExtensions, 5 WebpackModuleFunc 6} from "@moonlight-mod/types"; 7import { readConfig } from "../config"; 8import Logger from "../util/logger"; 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 if (wp.run == null && ext.scripts.webpackModules?.[name] != null) { 54 const func = new Function( 55 "module", 56 "exports", 57 "require", 58 ext.scripts.webpackModules[name]! 59 ) as WebpackModuleFunc; 60 registerWebpackModule({ 61 ...wp, 62 ext: ext.id, 63 id: name, 64 run: func 65 }); 66 } else { 67 registerWebpackModule({ ...wp, ext: ext.id, id: name }); 68 } 69 } 70 } 71 } 72 } 73 74 nodePreload: { 75 if (ext.scripts.nodePath != null) { 76 try { 77 const module = require(ext.scripts.nodePath); 78 moonlightNode.nativesCache[ext.id] = module; 79 } catch (e) { 80 logger.error(`Failed to load extension "${ext.id}"`, e); 81 } 82 } 83 } 84 85 injector: { 86 if (ext.scripts.hostPath != null) { 87 try { 88 require(ext.scripts.hostPath); 89 } catch (e) { 90 logger.error(`Failed to load extension "${ext.id}"`, e); 91 } 92 } 93 } 94} 95 96/* 97 This function resolves extensions and loads them, split into a few stages: 98 99 - Duplicate detection (removing multiple extensions with the same internal ID) 100 - Dependency resolution (creating a dependency graph & detecting circular dependencies) 101 - Failed dependency pruning 102 - Implicit dependency resolution (enabling extensions that are dependencies of other extensions) 103 - Loading all extensions 104 105 Instead of constructing an order from the dependency graph and loading 106 extensions synchronously, we load them in parallel asynchronously. Loading 107 extensions fires an event on completion, which allows us to await the loading 108 of another extension, resolving dependencies & load order effectively. 109*/ 110export async function loadExtensions( 111 exts: DetectedExtension[] 112): Promise<ProcessedExtensions> { 113 const items = exts 114 .map((ext) => { 115 return { 116 id: ext.id, 117 data: ext 118 }; 119 }) 120 .sort((a, b) => a.id.localeCompare(b.id)); 121 122 const [sorted, dependencyGraph] = calculateDependencies( 123 items, 124 125 function fetchDep(id) { 126 return exts.find((x) => x.id === id) ?? null; 127 }, 128 129 function getDeps(item) { 130 return item.data.manifest.dependencies ?? []; 131 }, 132 133 function getIncompatible(item) { 134 return item.data.manifest.incompatible ?? []; 135 } 136 ); 137 exts = sorted.map((x) => x.data); 138 139 logger.debug( 140 "Implicit dependency stage - extension list:", 141 exts.map((x) => x.id) 142 ); 143 const config = readConfig(); 144 const implicitlyEnabled: string[] = []; 145 146 function isEnabledInConfig(ext: DetectedExtension) { 147 if (implicitlyEnabled.includes(ext.id)) return true; 148 149 const entry = config.extensions[ext.id]; 150 if (entry == null) return false; 151 152 if (entry === true) return true; 153 if (typeof entry === "object" && entry.enabled === true) return true; 154 155 return false; 156 } 157 158 function validateDeps(ext: DetectedExtension) { 159 if (isEnabledInConfig(ext)) { 160 const deps = dependencyGraph.get(ext.id)!; 161 for (const dep of deps.values()) { 162 validateDeps(exts.find((e) => e.id === dep)!); 163 } 164 } else { 165 const dependsOnMe = Array.from(dependencyGraph.entries()).filter( 166 ([, v]) => v?.has(ext.id) 167 ); 168 169 if (dependsOnMe.length > 0) { 170 logger.debug("Implicitly enabling extension", ext.id); 171 implicitlyEnabled.push(ext.id); 172 } 173 } 174 } 175 176 for (const ext of exts) validateDeps(ext); 177 exts = exts.filter((e) => isEnabledInConfig(e)); 178 179 return { 180 extensions: exts, 181 dependencyGraph 182 }; 183} 184 185export async function loadProcessedExtensions({ 186 extensions, 187 dependencyGraph 188}: ProcessedExtensions) { 189 const eventEmitter = createEventEmitter(); 190 const finished: Set<string> = new Set(); 191 192 logger.debug( 193 "Load stage - extension list:", 194 extensions.map((x) => x.id) 195 ); 196 197 async function loadExtWithDependencies(ext: DetectedExtension) { 198 const deps = Array.from(dependencyGraph.get(ext.id)!); 199 200 // Wait for the dependencies to finish 201 const waitPromises = deps.map( 202 (dep: string) => 203 new Promise<void>((r) => { 204 function cb(eventDep: string) { 205 if (eventDep === dep) { 206 done(); 207 } 208 } 209 210 function done() { 211 eventEmitter.removeEventListener("ext-ready", cb); 212 r(); 213 } 214 215 eventEmitter.addEventListener("ext-ready", cb); 216 if (finished.has(dep)) done(); 217 }) 218 ); 219 220 if (waitPromises.length > 0) { 221 logger.debug( 222 `Waiting on ${waitPromises.length} dependencies for "${ext.id}"` 223 ); 224 await Promise.all(waitPromises); 225 } 226 227 logger.debug(`Loading "${ext.id}"`); 228 await loadExt(ext); 229 230 finished.add(ext.id); 231 eventEmitter.dispatchEvent("ext-ready", ext.id); 232 logger.debug(`Loaded "${ext.id}"`); 233 } 234 235 webPreload: { 236 for (const ext of extensions) { 237 moonlight.enabledExtensions.add(ext.id); 238 } 239 } 240 241 logger.debug("Loading all extensions"); 242 await Promise.all(extensions.map(loadExtWithDependencies)); 243 logger.info(`Loaded ${extensions.length} extensions`); 244}