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 { 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}