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