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