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 + `\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}