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