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