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