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