+1
packages/core-extensions/package.json
+1
packages/core-extensions/package.json
+1
packages/core-extensions/src/appPanels/manifest.json
+1
packages/core-extensions/src/appPanels/manifest.json
+1
packages/core-extensions/src/common/manifest.json
+1
packages/core-extensions/src/common/manifest.json
+1
packages/core-extensions/src/contextMenu/manifest.json
+1
packages/core-extensions/src/contextMenu/manifest.json
+1
packages/core-extensions/src/devToolsExtensions/manifest.json
+1
packages/core-extensions/src/devToolsExtensions/manifest.json
+1
packages/core-extensions/src/disableSentry/manifest.json
+1
packages/core-extensions/src/disableSentry/manifest.json
-3
packages/core-extensions/src/experiments/index.ts
-3
packages/core-extensions/src/experiments/index.ts
···
17
17
},
18
18
19
19
// Enable staff help menu
20
-
// FIXME: either make this actually work live (needs a state hook) or just
21
-
// wait for #122
22
20
{
23
21
find: ".HEADER_BAR)",
24
22
replace: {
···
29
27
},
30
28
31
29
// Enable further staff-locked options
32
-
// FIXME: #122, this doesn't work live
33
30
{
34
31
find: "shouldShowLurkerModeUpsellPopout:",
35
32
replace: {
+3
packages/core-extensions/src/experiments/manifest.json
+3
packages/core-extensions/src/experiments/manifest.json
···
1
1
{
2
+
"$schema": "https://moonlight-mod.github.io/manifest.schema.json",
2
3
"id": "experiments",
3
4
"apiLevel": 2,
4
5
"meta": {
···
9
10
},
10
11
"settings": {
11
12
"devtools": {
13
+
"advice": "reload",
12
14
"displayName": "Enable staff help menu (DevTools)",
13
15
"type": "boolean",
14
16
"default": false
15
17
},
16
18
"staffSettings": {
19
+
"advice": "reload",
17
20
"displayName": "Allow access to other staff settings elsewhere",
18
21
"type": "boolean",
19
22
"default": false
+1
packages/core-extensions/src/markdown/manifest.json
+1
packages/core-extensions/src/markdown/manifest.json
+77
packages/core-extensions/src/moonbase/host.ts
+77
packages/core-extensions/src/moonbase/host.ts
···
1
+
import * as electron from "electron";
2
+
import * as fs from "node:fs/promises";
3
+
import * as path from "node:path";
4
+
import getNatives from "./native";
5
+
6
+
const natives = getNatives();
7
+
8
+
const confirm = (action: string) =>
9
+
electron.dialog
10
+
.showMessageBox({
11
+
title: "Are you sure?",
12
+
message: `Are you sure? This will ${action} and restart Discord.`,
13
+
type: "warning",
14
+
buttons: ["OK", "Cancel"]
15
+
})
16
+
.then((r) => r.response === 0);
17
+
18
+
async function updateAndRestart() {
19
+
if (!(await confirm("update moonlight"))) return;
20
+
const newVersion = await natives.checkForMoonlightUpdate();
21
+
22
+
if (newVersion === null) {
23
+
electron.dialog.showMessageBox({ message: "You are already on the latest version of moonlight." });
24
+
return;
25
+
}
26
+
27
+
try {
28
+
await natives.updateMoonlight();
29
+
await electron.dialog.showMessageBox({ message: "Update successful, restarting Discord." });
30
+
electron.app.relaunch();
31
+
electron.app.exit(0);
32
+
} catch {
33
+
await electron.dialog.showMessageBox({
34
+
message: "Failed to update moonlight. Please use the installer instead.",
35
+
type: "error"
36
+
});
37
+
}
38
+
}
39
+
40
+
async function resetConfig() {
41
+
if (!(await confirm("reset your configuration"))) return;
42
+
43
+
const config = await moonlightHost.getConfigPath();
44
+
const dir = path.dirname(config);
45
+
const branch = path.basename(config, ".json");
46
+
await fs.rename(config, path.join(dir, `${branch}-backup-${Math.floor(Date.now() / 1000)}.json`));
47
+
48
+
await electron.dialog.showMessageBox({ message: "Configuration reset, restarting Discord." });
49
+
electron.app.relaunch();
50
+
electron.app.exit(0);
51
+
}
52
+
53
+
function showAbout() {
54
+
electron.dialog.showMessageBox({
55
+
title: "About moonlight",
56
+
message: `moonlight ${moonlightHost.branch} ${moonlightHost.version}`
57
+
});
58
+
}
59
+
60
+
electron.app.whenReady().then(() => {
61
+
const original = electron.Menu.buildFromTemplate;
62
+
electron.Menu.buildFromTemplate = function (entries) {
63
+
const i = entries.findIndex((e) => e.label === "Check for Updates...");
64
+
if (i === -1) return original.call(this, entries);
65
+
66
+
entries.splice(i + 1, 0, {
67
+
label: "moonlight",
68
+
submenu: [
69
+
{ label: "Update and restart", click: updateAndRestart },
70
+
{ label: "Reset config", click: resetConfig },
71
+
{ label: "About", click: showAbout }
72
+
]
73
+
});
74
+
75
+
return original.call(this, entries);
76
+
};
77
+
});
+7
-2
packages/core-extensions/src/moonbase/manifest.json
+7
-2
packages/core-extensions/src/moonbase/manifest.json
···
1
1
{
2
+
"$schema": "https://moonlight-mod.github.io/manifest.schema.json",
2
3
"id": "moonbase",
3
4
"apiLevel": 2,
4
5
"meta": {
···
9
10
"dependencies": ["spacepack", "settings", "common", "notices"],
10
11
"settings": {
11
12
"sections": {
13
+
"advice": "reload",
12
14
"displayName": "Split into sections",
13
15
"description": "Show the Moonbase tabs as separate sections",
14
16
"type": "boolean"
15
17
},
16
18
"saveFilter": {
19
+
"advice": "none",
17
20
"displayName": "Persist filter",
18
21
"description": "Save extension filter in config",
19
22
"type": "boolean"
20
23
},
21
24
"updateChecking": {
25
+
"advice": "none",
22
26
"displayName": "Automatic update checking",
23
27
"description": "Checks for updates to moonlight",
24
28
"type": "boolean",
25
-
"default": "true"
29
+
"default": true
26
30
},
27
31
"updateBanner": {
32
+
"advice": "none",
28
33
"displayName": "Show update banner",
29
34
"description": "Shows a banner for moonlight and extension updates",
30
35
"type": "boolean",
31
-
"default": "true"
36
+
"default": true
32
37
}
33
38
},
34
39
"cors": [
+14
-12
packages/core-extensions/src/moonbase/native.ts
+14
-12
packages/core-extensions/src/moonbase/native.ts
···
4
4
import { distDir, repoUrlFile, installedVersionFile } from "@moonlight-mod/types/constants";
5
5
import { parseTarGzip } from "nanotar";
6
6
7
+
const moonlightGlobal = globalThis.moonlightHost ?? globalThis.moonlightNode;
8
+
7
9
const githubRepo = "moonlight-mod/moonlight";
8
10
const githubApiUrl = `https://api.github.com/repos/${githubRepo}/releases/latest`;
9
11
const artifactName = "dist.tar.gz";
···
11
13
const nightlyRefUrl = "https://moonlight-mod.github.io/moonlight/ref";
12
14
const nightlyZipUrl = "https://moonlight-mod.github.io/moonlight/dist.tar.gz";
13
15
14
-
export const userAgent = `moonlight/${moonlightNode.version} (https://github.com/moonlight-mod/moonlight)`;
16
+
export const userAgent = `moonlight/${moonlightGlobal.version} (https://github.com/moonlight-mod/moonlight)`;
15
17
16
18
async function getStableRelease(): Promise<{
17
19
name: string;
···
30
32
}
31
33
32
34
export default function getNatives(): MoonbaseNatives {
33
-
const logger = moonlightNode.getLogger("moonbase/natives");
35
+
const logger = moonlightGlobal.getLogger("moonbase/natives");
34
36
35
37
return {
36
38
async checkForMoonlightUpdate() {
37
39
try {
38
-
if (moonlightNode.branch === MoonlightBranch.STABLE) {
40
+
if (moonlightGlobal.branch === MoonlightBranch.STABLE) {
39
41
const json = await getStableRelease();
40
-
return json.name !== moonlightNode.version ? json.name : null;
41
-
} else if (moonlightNode.branch === MoonlightBranch.NIGHTLY) {
42
+
return json.name !== moonlightGlobal.version ? json.name : null;
43
+
} else if (moonlightGlobal.branch === MoonlightBranch.NIGHTLY) {
42
44
const req = await fetch(nightlyRefUrl, {
43
45
cache: "no-store",
44
46
headers: {
···
46
48
}
47
49
});
48
50
const ref = (await req.text()).split("\n")[0];
49
-
return ref !== moonlightNode.version ? ref : null;
51
+
return ref !== moonlightGlobal.version ? ref : null;
50
52
}
51
53
52
54
return null;
···
96
98
}
97
99
98
100
const [tar, ref] =
99
-
moonlightNode.branch === MoonlightBranch.STABLE
101
+
moonlightGlobal.branch === MoonlightBranch.STABLE
100
102
? await downloadStable()
101
-
: moonlightNode.branch === MoonlightBranch.NIGHTLY
103
+
: moonlightGlobal.branch === MoonlightBranch.NIGHTLY
102
104
? await downloadNightly()
103
105
: [null, null];
104
106
105
107
if (!tar || !ref) return;
106
108
107
-
const dist = moonlightNodeSandboxed.fs.join(moonlightNode.getMoonlightDir(), distDir);
109
+
const dist = moonlightNodeSandboxed.fs.join(moonlightGlobal.getMoonlightDir(), distDir);
108
110
if (await moonlightNodeSandboxed.fs.exists(dist)) await moonlightNodeSandboxed.fs.rmdir(dist);
109
111
await moonlightNodeSandboxed.fs.mkdir(dist);
110
112
···
122
124
}
123
125
124
126
logger.debug("Writing version file:", ref);
125
-
const versionFile = moonlightNodeSandboxed.fs.join(moonlightNode.getMoonlightDir(), installedVersionFile);
127
+
const versionFile = moonlightNodeSandboxed.fs.join(moonlightGlobal.getMoonlightDir(), installedVersionFile);
126
128
await moonlightNodeSandboxed.fs.writeFileString(versionFile, ref.trim());
127
129
128
130
logger.debug("Update extracted");
···
157
159
}
158
160
});
159
161
160
-
const dir = moonlightNode.getExtensionDir(manifest.id);
162
+
const dir = moonlightGlobal.getExtensionDir(manifest.id);
161
163
// remake it in case of updates
162
164
if (await moonlightNodeSandboxed.fs.exists(dir)) await moonlightNodeSandboxed.fs.rmdir(dir);
163
165
await moonlightNodeSandboxed.fs.mkdir(dir);
···
176
178
},
177
179
178
180
async deleteExtension(id) {
179
-
const dir = moonlightNode.getExtensionDir(id);
181
+
const dir = moonlightGlobal.getExtensionDir(id);
180
182
await moonlightNodeSandboxed.fs.rmdir(dir);
181
183
}
182
184
};
+12
packages/core-extensions/src/moonbase/style.css
+12
packages/core-extensions/src/moonbase/style.css
···
7
7
margin-top: 0px;
8
8
}
9
9
10
+
.moonbase-retry-button {
11
+
padding: 8px;
12
+
margin-right: 8px;
13
+
}
14
+
10
15
textarea.moonbase-resizeable {
11
16
resize: vertical;
12
17
}
···
37
42
display: flex;
38
43
flex-direction: row;
39
44
justify-content: space-between;
45
+
}
46
+
47
+
.moonbase-help-message-sticky {
48
+
position: sticky;
49
+
top: 24px;
50
+
z-index: 10;
51
+
background-color: var(--background-primary);
40
52
}
41
53
42
54
.moonbase-extension-update-section {
+8
packages/core-extensions/src/moonbase/types.ts
+8
packages/core-extensions/src/moonbase/types.ts
+2
-3
packages/core-extensions/src/moonbase/webpackModules/settings.tsx
+2
-3
packages/core-extensions/src/moonbase/webpackModules/settings.tsx
···
1
1
import settings from "@moonlight-mod/wp/settings_settings";
2
2
import React from "@moonlight-mod/wp/react";
3
3
import spacepack from "@moonlight-mod/wp/spacepack_spacepack";
4
-
import { Moonbase, pages } from "@moonlight-mod/wp/moonbase_ui";
4
+
import { Moonbase, pages, RestartAdviceMessage, Update } from "@moonlight-mod/wp/moonbase_ui";
5
5
6
6
import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores";
7
7
import * as Components from "@moonlight-mod/wp/discord/components/common/index";
8
-
9
-
import Update from "./ui/update";
10
8
11
9
const { MenuItem, Text, Breadcrumbs } = Components;
12
10
···
72
70
{page.name}
73
71
</Breadcrumbs>
74
72
73
+
<RestartAdviceMessage />
75
74
<Update />
76
75
77
76
<page.element />
+168
-72
packages/core-extensions/src/moonbase/webpackModules/stores.ts
+168
-72
packages/core-extensions/src/moonbase/webpackModules/stores.ts
···
1
-
import { Config, ExtensionLoadSource } from "@moonlight-mod/types";
2
-
import { ExtensionState, MoonbaseExtension, MoonbaseNatives, RepositoryManifest } from "../types";
1
+
import { Config, ExtensionEnvironment, ExtensionLoadSource, ExtensionSettingsAdvice } from "@moonlight-mod/types";
2
+
import { ExtensionState, MoonbaseExtension, MoonbaseNatives, RepositoryManifest, RestartAdvice } from "../types";
3
3
import { Store } from "@moonlight-mod/wp/discord/packages/flux";
4
4
import Dispatcher from "@moonlight-mod/wp/discord/Dispatcher";
5
5
import getNatives from "../native";
···
7
7
import { checkExtensionCompat, ExtensionCompat } from "@moonlight-mod/core/extension/loader";
8
8
import { CustomComponent } from "@moonlight-mod/types/coreExtensions/moonbase";
9
9
import { getConfigOption, setConfigOption } from "@moonlight-mod/core/util/config";
10
+
import diff from "microdiff";
10
11
11
12
const logger = moonlight.getLogger("moonbase");
12
13
···
14
15
if (moonlightNode.isBrowser) natives = getNatives();
15
16
16
17
class MoonbaseSettingsStore extends Store<any> {
17
-
private origConfig: Config;
18
+
private initialConfig: Config;
19
+
private savedConfig: Config;
18
20
private config: Config;
19
21
private extensionIndex: number;
20
22
private configComponents: Record<string, Record<string, CustomComponent>> = {};
···
35
37
return this.#showOnlyUpdateable;
36
38
}
37
39
40
+
restartAdvice = RestartAdvice.NotNeeded;
41
+
38
42
extensions: { [id: number]: MoonbaseExtension };
39
43
updates: {
40
44
[id: number]: {
···
47
51
constructor() {
48
52
super(Dispatcher);
49
53
50
-
this.origConfig = moonlightNode.config;
51
-
this.config = this.clone(this.origConfig);
54
+
this.initialConfig = moonlightNode.config;
55
+
this.savedConfig = moonlightNode.config;
56
+
this.config = this.clone(this.savedConfig);
52
57
this.extensionIndex = 0;
53
58
54
59
this.modified = false;
···
71
76
};
72
77
}
73
78
74
-
natives!
75
-
.fetchRepositories(this.config.repositories)
76
-
.then((ret) => {
77
-
for (const [repo, exts] of Object.entries(ret)) {
78
-
try {
79
-
for (const ext of exts) {
80
-
const uniqueId = this.extensionIndex++;
81
-
const extensionData = {
82
-
id: ext.id,
83
-
uniqueId,
84
-
manifest: ext,
85
-
source: { type: ExtensionLoadSource.Normal, url: repo },
86
-
state: ExtensionState.NotDownloaded,
87
-
compat: ExtensionCompat.Compatible,
88
-
hasUpdate: false
89
-
};
79
+
this.checkUpdates();
80
+
}
90
81
91
-
// Don't present incompatible updates
92
-
if (checkExtensionCompat(ext) !== ExtensionCompat.Compatible) continue;
82
+
async checkUpdates() {
83
+
await Promise.all([this.checkExtensionUpdates(), this.checkMoonlightUpdates()]);
84
+
this.shouldShowNotice = this.newVersion != null || Object.keys(this.updates).length > 0;
85
+
this.emitChange();
86
+
}
93
87
94
-
const existing = this.getExisting(extensionData);
95
-
if (existing != null) {
96
-
// Make sure the download URL is properly updated
97
-
for (const [id, e] of Object.entries(this.extensions)) {
98
-
if (e.id === ext.id && e.source.url === repo) {
99
-
this.extensions[parseInt(id)].manifest = {
100
-
...e.manifest,
101
-
download: ext.download
102
-
};
103
-
break;
104
-
}
105
-
}
88
+
private async checkExtensionUpdates() {
89
+
const repositories = await natives!.fetchRepositories(this.savedConfig.repositories);
106
90
107
-
if (this.hasUpdate(extensionData)) {
108
-
this.updates[existing.uniqueId] = {
109
-
version: ext.version!,
110
-
download: ext.download,
111
-
updateManifest: ext
112
-
};
113
-
existing.hasUpdate = true;
114
-
existing.changelog = ext.meta?.changelog;
115
-
}
91
+
// Reset update state
92
+
for (const id in this.extensions) {
93
+
const ext = this.extensions[id];
94
+
ext.hasUpdate = false;
95
+
ext.changelog = undefined;
96
+
}
97
+
this.updates = {};
116
98
117
-
continue;
118
-
}
99
+
for (const [repo, exts] of Object.entries(repositories)) {
100
+
for (const ext of exts) {
101
+
const uniqueId = this.extensionIndex++;
102
+
const extensionData = {
103
+
id: ext.id,
104
+
uniqueId,
105
+
manifest: ext,
106
+
source: { type: ExtensionLoadSource.Normal, url: repo },
107
+
state: ExtensionState.NotDownloaded,
108
+
compat: ExtensionCompat.Compatible,
109
+
hasUpdate: false
110
+
};
119
111
120
-
this.extensions[uniqueId] = extensionData;
121
-
}
122
-
} catch (e) {
123
-
logger.error(`Error processing repository ${repo}`, e);
112
+
// Don't present incompatible updates
113
+
if (checkExtensionCompat(ext) !== ExtensionCompat.Compatible) continue;
114
+
115
+
const existing = this.getExisting(extensionData);
116
+
if (existing != null) {
117
+
// Make sure the download URL is properly updated
118
+
existing.manifest = {
119
+
...existing.manifest,
120
+
download: ext.download
121
+
};
122
+
123
+
if (this.hasUpdate(extensionData)) {
124
+
this.updates[existing.uniqueId] = {
125
+
version: ext.version!,
126
+
download: ext.download,
127
+
updateManifest: ext
128
+
};
129
+
existing.hasUpdate = true;
130
+
existing.changelog = ext.meta?.changelog;
124
131
}
132
+
} else {
133
+
this.extensions[uniqueId] = extensionData;
125
134
}
135
+
}
136
+
}
137
+
}
126
138
127
-
this.emitChange();
128
-
})
129
-
.then(() =>
130
-
this.getExtensionConfigRaw("moonbase", "updateChecking", true)
131
-
? natives!.checkForMoonlightUpdate()
132
-
: new Promise<null>((resolve) => resolve(null))
133
-
)
134
-
.then((version) => {
135
-
this.newVersion = version;
136
-
this.emitChange();
137
-
})
138
-
.then(() => {
139
-
this.shouldShowNotice = this.newVersion != null || Object.keys(this.updates).length > 0;
140
-
this.emitChange();
141
-
});
139
+
private async checkMoonlightUpdates() {
140
+
this.newVersion = this.getExtensionConfigRaw("moonbase", "updateChecking", true)
141
+
? await natives!.checkForMoonlightUpdate()
142
+
: null;
142
143
}
143
144
144
145
private getExisting(ext: MoonbaseExtension) {
···
154
155
155
156
// Jank
156
157
private isModified() {
157
-
const orig = JSON.stringify(this.origConfig);
158
+
const orig = JSON.stringify(this.savedConfig);
158
159
const curr = JSON.stringify(this.config);
159
160
return orig !== curr;
160
161
}
···
269
270
}
270
271
271
272
if (update != null) {
272
-
this.extensions[uniqueId].settingsOverride = update.updateManifest.settings;
273
-
this.extensions[uniqueId].compat = checkExtensionCompat(update.updateManifest);
273
+
const existing = this.extensions[uniqueId];
274
+
existing.settingsOverride = update.updateManifest.settings;
275
+
existing.compat = checkExtensionCompat(update.updateManifest);
276
+
existing.manifest = update.updateManifest;
277
+
existing.hasUpdate = false;
278
+
existing.changelog = update.updateManifest.meta?.changelog;
274
279
}
275
280
276
281
delete this.updates[uniqueId];
···
279
284
}
280
285
281
286
this.installing = false;
287
+
this.restartAdvice = this.#computeRestartAdvice();
282
288
this.emitChange();
283
289
}
284
290
···
310
316
const aRank = this.getRank(a);
311
317
const bRank = this.getRank(b);
312
318
if (aRank === bRank) {
313
-
const repoIndex = this.config.repositories.indexOf(a.source.url!);
314
-
const otherRepoIndex = this.config.repositories.indexOf(b.source.url!);
319
+
const repoIndex = this.savedConfig.repositories.indexOf(a.source.url!);
320
+
const otherRepoIndex = this.savedConfig.repositories.indexOf(b.source.url!);
315
321
return repoIndex - otherRepoIndex;
316
322
} else {
317
323
return bRank - aRank;
···
335
341
}
336
342
337
343
this.installing = false;
344
+
this.restartAdvice = this.#computeRestartAdvice();
338
345
this.emitChange();
339
346
}
340
347
···
366
373
return this.configComponents[ext]?.[name];
367
374
}
368
375
376
+
#computeRestartAdvice() {
377
+
const i = this.initialConfig; // Initial config, from startup
378
+
const n = this.config; // New config about to be saved
379
+
380
+
let returnedAdvice = RestartAdvice.NotNeeded;
381
+
const updateAdvice = (r: RestartAdvice) => (returnedAdvice < r ? (returnedAdvice = r) : returnedAdvice);
382
+
383
+
// Top-level keys, repositories is not needed here because Moonbase handles it.
384
+
if (i.patchAll !== n.patchAll) updateAdvice(RestartAdvice.ReloadNeeded);
385
+
if (i.loggerLevel !== n.loggerLevel) updateAdvice(RestartAdvice.ReloadNeeded);
386
+
if (diff(i.devSearchPaths ?? [], n.devSearchPaths ?? [], { cyclesFix: false }).length !== 0)
387
+
return updateAdvice(RestartAdvice.RestartNeeded);
388
+
389
+
// Extension specific logic
390
+
for (const id in n.extensions) {
391
+
// Installed extension (might not be detected yet)
392
+
const ext = Object.values(this.extensions).find((e) => e.id === id && e.state !== ExtensionState.NotDownloaded);
393
+
// Installed and detected extension
394
+
const detected = moonlightNode.extensions.find((e) => e.id === id);
395
+
396
+
// If it's not installed at all, we don't care
397
+
if (!ext) continue;
398
+
399
+
const initState = i.extensions[id];
400
+
const newState = n.extensions[id];
401
+
402
+
const newEnabled = typeof newState === "boolean" ? newState : newState.enabled;
403
+
// If it's enabled but not detected yet, restart.
404
+
if (newEnabled && !detected) {
405
+
return updateAdvice(RestartAdvice.RestartNeeded);
406
+
continue;
407
+
}
408
+
409
+
// Toggling extensions specifically wants to rely on the initial state,
410
+
// that's what was considered when loading extensions.
411
+
const initEnabled = initState && (typeof initState === "boolean" ? initState : initState.enabled);
412
+
if (initEnabled !== newEnabled) {
413
+
// If we have the extension locally, we confidently know if it has host/preload scripts.
414
+
// If not, we have to respect the environment specified in the manifest.
415
+
// If that is the default, we can't know what's needed.
416
+
417
+
if (detected?.scripts.hostPath || detected?.scripts.nodePath) {
418
+
return updateAdvice(RestartAdvice.RestartNeeded);
419
+
}
420
+
421
+
switch (ext.manifest.environment) {
422
+
case ExtensionEnvironment.Both:
423
+
case ExtensionEnvironment.Web:
424
+
updateAdvice(RestartAdvice.ReloadNeeded);
425
+
continue;
426
+
case ExtensionEnvironment.Desktop:
427
+
return updateAdvice(RestartAdvice.RestartNeeded);
428
+
default:
429
+
updateAdvice(RestartAdvice.ReloadNeeded);
430
+
continue;
431
+
}
432
+
}
433
+
434
+
const initConfig = typeof initState === "boolean" ? {} : initState.config ?? {};
435
+
const newConfig = typeof newState === "boolean" ? {} : newState.config ?? {};
436
+
437
+
const def = ext.manifest.settings;
438
+
if (!def) continue;
439
+
440
+
const changedKeys = diff(initConfig, newConfig, { cyclesFix: false }).map((c) => c.path[0]);
441
+
for (const key in def) {
442
+
if (!changedKeys.includes(key)) continue;
443
+
444
+
const advice = def[key].advice;
445
+
switch (advice) {
446
+
case ExtensionSettingsAdvice.None:
447
+
updateAdvice(RestartAdvice.NotNeeded);
448
+
continue;
449
+
case ExtensionSettingsAdvice.Reload:
450
+
updateAdvice(RestartAdvice.ReloadNeeded);
451
+
continue;
452
+
case ExtensionSettingsAdvice.Restart:
453
+
updateAdvice(RestartAdvice.RestartNeeded);
454
+
continue;
455
+
default:
456
+
updateAdvice(RestartAdvice.ReloadSuggested);
457
+
}
458
+
}
459
+
}
460
+
461
+
return returnedAdvice;
462
+
}
463
+
369
464
writeConfig() {
370
465
this.submitting = true;
466
+
this.restartAdvice = this.#computeRestartAdvice();
371
467
372
468
moonlightNode.writeConfig(this.config);
373
-
this.origConfig = this.clone(this.config);
469
+
this.savedConfig = this.clone(this.config);
374
470
375
471
this.submitting = false;
376
472
this.modified = false;
···
380
476
reset() {
381
477
this.submitting = false;
382
478
this.modified = false;
383
-
this.config = this.clone(this.origConfig);
479
+
this.config = this.clone(this.savedConfig);
384
480
this.emitChange();
385
481
}
386
482
+4
-2
packages/core-extensions/src/moonbase/webpackModules/ui/HelpMessage.tsx
+4
-2
packages/core-extensions/src/moonbase/webpackModules/ui/HelpMessage.tsx
···
11
11
className,
12
12
text,
13
13
icon,
14
-
children
14
+
children,
15
+
type = "info"
15
16
}: {
16
17
className?: string;
17
18
text: string;
18
19
icon: React.ComponentType<any>;
20
+
type?: "warning" | "positive" | "error" | "info";
19
21
children?: React.ReactNode;
20
22
}) {
21
23
return (
22
24
<div
23
-
className={`${Margins.marginBottom20} ${HelpMessageClasses.info} ${HelpMessageClasses.container} moonbase-help-message ${className}`}
25
+
className={`${Margins.marginBottom20} ${HelpMessageClasses[type]} ${HelpMessageClasses.container} moonbase-help-message ${className}`}
24
26
>
25
27
<Flex direction={Flex.Direction.HORIZONTAL}>
26
28
<div
+47
packages/core-extensions/src/moonbase/webpackModules/ui/RestartAdvice.tsx
+47
packages/core-extensions/src/moonbase/webpackModules/ui/RestartAdvice.tsx
···
1
+
import { useStateFromStores } from "@moonlight-mod/wp/discord/packages/flux";
2
+
import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores";
3
+
import * as Components from "@moonlight-mod/wp/discord/components/common/index";
4
+
import React from "@moonlight-mod/wp/react";
5
+
import { RestartAdvice } from "../../types";
6
+
import HelpMessage from "./HelpMessage";
7
+
8
+
const { Button } = Components;
9
+
10
+
const strings: Record<RestartAdvice, string> = {
11
+
[RestartAdvice.NotNeeded]: "how did you even",
12
+
[RestartAdvice.ReloadSuggested]: "A reload might be needed to apply some of the changed options.",
13
+
[RestartAdvice.ReloadNeeded]: "A reload is needed to apply some of the changed options.",
14
+
[RestartAdvice.RestartNeeded]: "A restart is needed to apply some of the changed options."
15
+
};
16
+
17
+
const buttonStrings: Record<RestartAdvice, string> = {
18
+
[RestartAdvice.NotNeeded]: "huh?",
19
+
[RestartAdvice.ReloadSuggested]: "Reload",
20
+
[RestartAdvice.ReloadNeeded]: "Reload",
21
+
[RestartAdvice.RestartNeeded]: "Restart"
22
+
};
23
+
24
+
const actions: Record<RestartAdvice, () => void> = {
25
+
[RestartAdvice.NotNeeded]: () => {},
26
+
[RestartAdvice.ReloadSuggested]: () => window.location.reload(),
27
+
[RestartAdvice.ReloadNeeded]: () => window.location.reload(),
28
+
[RestartAdvice.RestartNeeded]: () => MoonbaseSettingsStore.restartDiscord()
29
+
};
30
+
31
+
const { CircleWarningIcon } = Components;
32
+
33
+
export default function RestartAdviceMessage() {
34
+
const restartAdvice = useStateFromStores([MoonbaseSettingsStore], () => MoonbaseSettingsStore.restartAdvice);
35
+
36
+
if (restartAdvice === RestartAdvice.NotNeeded) return null;
37
+
38
+
return (
39
+
<div className="moonbase-help-message-sticky">
40
+
<HelpMessage text={strings[restartAdvice]} icon={CircleWarningIcon} type="warning">
41
+
<Button color={Button.Colors.YELLOW} size={Button.Sizes.TINY} onClick={actions[restartAdvice]}>
42
+
{buttonStrings[restartAdvice]}
43
+
</Button>
44
+
</HelpMessage>
45
+
</div>
46
+
);
47
+
}
+2
-17
packages/core-extensions/src/moonbase/webpackModules/ui/extensions/card.tsx
+2
-17
packages/core-extensions/src/moonbase/webpackModules/ui/extensions/card.tsx
···
23
23
24
24
import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores";
25
25
26
-
const { BeakerIcon, DownloadIcon, TrashIcon, AngleBracketsIcon, CircleWarningIcon, Tooltip } = Components;
26
+
const { BeakerIcon, DownloadIcon, TrashIcon, AngleBracketsIcon, Tooltip } = Components;
27
27
28
28
const PanelButton = spacepack.findByCode("Masks.PANEL_BUTTON")[0].exports.Z;
29
29
const TabBarClasses = spacepack.findByExports("tabBar", "tabBarItem", "headerContentWrapper")[0].exports;
···
40
40
41
41
export default function ExtensionCard({ uniqueId }: { uniqueId: number }) {
42
42
const [tab, setTab] = React.useState(ExtensionPage.Info);
43
-
const [restartNeeded, setRestartNeeded] = React.useState(false);
44
43
45
-
const { ext, enabled, busy, update, conflicting, showingNotice } = useStateFromStores([MoonbaseSettingsStore], () => {
44
+
const { ext, enabled, busy, update, conflicting } = useStateFromStores([MoonbaseSettingsStore], () => {
46
45
return {
47
46
ext: MoonbaseSettingsStore.getExtension(uniqueId),
48
47
enabled: MoonbaseSettingsStore.getExtensionEnabled(uniqueId),
49
48
busy: MoonbaseSettingsStore.busy,
50
-
showingNotice: MoonbaseSettingsStore.showNotice(),
51
49
update: MoonbaseSettingsStore.getExtensionUpdate(uniqueId),
52
50
conflicting: MoonbaseSettingsStore.getExtensionConflicting(uniqueId)
53
51
};
···
111
109
/>
112
110
)}
113
111
114
-
{restartNeeded && !showingNotice && (
115
-
<PanelButton
116
-
icon={() => <CircleWarningIcon color={Components.tokens.colors.STATUS_DANGER} />}
117
-
onClick={() => MoonbaseSettingsStore.restartDiscord()}
118
-
tooltipText="You will need to reload/restart your client for this extension to work properly."
119
-
/>
120
-
)}
121
-
122
112
{ext.state === ExtensionState.NotDownloaded ? (
123
113
<Tooltip
124
114
text={conflicting ? CONFLICTING_TEXT : COMPAT_TEXT_MAP[ext.compat]}
···
141
131
if (!ext.manifest?.meta?.tags?.includes(ExtensionTag.DangerZone)) {
142
132
MoonbaseSettingsStore.setExtensionEnabled(uniqueId, true);
143
133
}
144
-
145
-
setRestartNeeded(true);
146
134
}}
147
135
>
148
136
Install
···
157
145
tooltipText="Delete"
158
146
onClick={() => {
159
147
MoonbaseSettingsStore.deleteExtension(uniqueId);
160
-
setRestartNeeded(true);
161
148
}}
162
149
/>
163
150
)}
···
168
155
tooltipText="Update"
169
156
onClick={() => {
170
157
MoonbaseSettingsStore.installExtension(uniqueId);
171
-
setRestartNeeded(true);
172
158
}}
173
159
/>
174
160
)}
···
190
176
onChange={() => {
191
177
const toggle = () => {
192
178
MoonbaseSettingsStore.setExtensionEnabled(uniqueId, !enabled);
193
-
setRestartNeeded(true);
194
179
};
195
180
196
181
if (enabled && constants.builtinExtensions.includes(ext.id)) {
+33
-2
packages/core-extensions/src/moonbase/webpackModules/ui/extensions/filterBar.tsx
+33
-2
packages/core-extensions/src/moonbase/webpackModules/ui/extensions/filterBar.tsx
···
16
16
MenuItem
17
17
} from "@moonlight-mod/wp/discord/components/common/index";
18
18
import * as Components from "@moonlight-mod/wp/discord/components/common/index";
19
+
import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores";
19
20
20
21
export enum Filter {
21
22
Core = 1 << 0,
···
44
45
const TagItem = spacepack.findByCode('"forum-tag-"')[0].exports.Z;
45
46
46
47
// FIXME: type component keys
47
-
const { ChevronSmallDownIcon, ChevronSmallUpIcon, ArrowsUpDownIcon } = Components;
48
+
const { ChevronSmallDownIcon, ChevronSmallUpIcon, ArrowsUpDownIcon, RetryIcon, Tooltip } = Components;
48
49
49
50
function toggleTag(selectedTags: Set<string>, setSelectedTags: (tags: Set<string>) => void, tag: string) {
50
51
const newState = new Set(selectedTags);
···
211
212
}
212
213
}
213
214
setTagsButtonOffset(offset);
214
-
}, [windowSize]);
215
+
}, [windowSize, tagsContainer.current, tagListInner.current, tagListInner.current?.getBoundingClientRect()?.width]);
215
216
216
217
return (
217
218
<div
···
221
222
}}
222
223
className={`${FilterBarClasses.tagsContainer} ${Margins.marginBottom8}`}
223
224
>
225
+
<Tooltip text="Refresh updates" position="top">
226
+
{(props: any) => (
227
+
<Button
228
+
{...props}
229
+
size={Button.Sizes.MIN}
230
+
color={Button.Colors.CUSTOM}
231
+
className={`${FilterBarClasses.sortDropdown} moonbase-retry-button`}
232
+
innerClassName={FilterBarClasses.sortDropdownInner}
233
+
onClick={() => MoonbaseSettingsStore.checkUpdates()}
234
+
>
235
+
<RetryIcon size={"custom"} width={16} />
236
+
</Button>
237
+
)}
238
+
</Tooltip>
224
239
<Popout
225
240
renderPopout={({ closePopout }: any) => (
226
241
<FilterButtonPopout filter={filter} setFilter={setFilter} closePopout={closePopout} />
···
305
320
</Button>
306
321
)}
307
322
</Popout>
323
+
<Button
324
+
size={Button.Sizes.MIN}
325
+
color={Button.Colors.CUSTOM}
326
+
className={`${FilterBarClasses.tagsButton} ${FilterBarClasses.tagsButtonPlaceholder}`}
327
+
innerClassName={FilterBarClasses.tagsButtonInner}
328
+
>
329
+
{selectedTags.size > 0 ? (
330
+
<div style={{ boxSizing: "content-box" }} className={FilterBarClasses.countContainer}>
331
+
<Text className={FilterBarClasses.countText} color="none" variant="text-xs/medium">
332
+
{selectedTags.size}
333
+
</Text>
334
+
</div>
335
+
) : null}
336
+
337
+
<ChevronSmallUpIcon size={"custom"} width={20} />
338
+
</Button>
308
339
</div>
309
340
);
310
341
}
+9
-13
packages/core-extensions/src/moonbase/webpackModules/ui/extensions/index.tsx
+9
-13
packages/core-extensions/src/moonbase/webpackModules/ui/extensions/index.tsx
···
13
13
import HelpMessage from "../HelpMessage";
14
14
15
15
const SearchBar: any = Object.values(spacepack.findByCode("hideSearchIcon")[0].exports)[0];
16
-
const { CircleInformationIcon, XSmallIcon } = Components;
16
+
const { FormDivider, CircleInformationIcon, XSmallIcon } = Components;
17
17
const PanelButton = spacepack.findByCode("Masks.PANEL_BUTTON")[0].exports.Z;
18
18
19
19
export default function ExtensionsPage() {
···
77
77
78
78
// Prioritize extensions with updates
79
79
const filteredWithUpdates = filtered.filter((ext) => ext!.hasUpdate);
80
-
const filteredWithoutUpdates = filtered.filter((ext) => !ext!.hasUpdate);
81
-
const { FormDivider } = Components;
80
+
const filterUpdates = showOnlyUpdateable && filteredWithUpdates.length > 0;
81
+
const filteredWithoutUpdates = filterUpdates ? [] : filtered.filter((ext) => !ext!.hasUpdate);
82
82
83
83
return (
84
84
<>
···
97
97
/>
98
98
<FilterBar filter={filter} setFilter={setFilter} selectedTags={selectedTags} setSelectedTags={setSelectedTags} />
99
99
100
-
{showOnlyUpdateable && (
100
+
{filterUpdates && (
101
101
<HelpMessage
102
102
icon={CircleInformationIcon}
103
103
text="Only displaying updates"
···
117
117
{filteredWithUpdates.map((ext) => (
118
118
<ExtensionCard uniqueId={ext.uniqueId} key={ext.uniqueId} />
119
119
))}
120
-
{!showOnlyUpdateable && (
121
-
<>
122
-
{filteredWithUpdates.length > 0 && filteredWithoutUpdates.length > 0 && (
123
-
<FormDivider className="moonbase-update-divider" />
124
-
)}
125
-
{filteredWithoutUpdates.map((ext) => (
126
-
<ExtensionCard uniqueId={ext.uniqueId} key={ext.uniqueId} />
127
-
))}
128
-
</>
120
+
{filteredWithUpdates.length > 0 && filteredWithoutUpdates.length > 0 && (
121
+
<FormDivider className="moonbase-update-divider" />
129
122
)}
123
+
{filteredWithoutUpdates.map((ext) => (
124
+
<ExtensionCard uniqueId={ext.uniqueId} key={ext.uniqueId} />
125
+
))}
130
126
</>
131
127
);
132
128
}
+4
packages/core-extensions/src/moonbase/webpackModules/ui/index.tsx
+4
packages/core-extensions/src/moonbase/webpackModules/ui/index.tsx
···
8
8
import ConfigPage from "./config";
9
9
import Update from "./update";
10
10
import { MoonbaseSettingsStore } from "@moonlight-mod/wp/moonbase_stores";
11
+
import RestartAdviceMessage from "./RestartAdvice";
11
12
12
13
const { Divider } = spacepack.findByCode(".forumOrHome]:")[0].exports.Z;
13
14
const TitleBarClasses = spacepack.findByCode("iconWrapper:", "children:")[0].exports;
···
66
67
</TabBar>
67
68
</div>
68
69
70
+
<RestartAdviceMessage />
69
71
<Update />
70
72
71
73
{React.createElement(pages[subsection].element)}
72
74
</>
73
75
);
74
76
}
77
+
78
+
export { RestartAdviceMessage, Update };
+7
packages/core-extensions/src/nativeFixes/manifest.json
+7
packages/core-extensions/src/nativeFixes/manifest.json
···
1
1
{
2
+
"$schema": "https://moonlight-mod.github.io/manifest.schema.json",
2
3
"id": "nativeFixes",
3
4
"meta": {
4
5
"name": "Native Fixes",
···
6
7
"authors": ["Cynosphere", "adryd"],
7
8
"tags": ["fixes"]
8
9
},
10
+
"environment": "desktop",
9
11
"settings": {
10
12
"devtoolsThemeFix": {
13
+
"advice": "restart",
11
14
"displayName": "Devtools Theme Fix",
12
15
"description": "Temporary workaround for devtools defaulting to light theme on Electron 32",
13
16
"type": "boolean",
14
17
"default": true
15
18
},
16
19
"disableRendererBackgrounding": {
20
+
"advice": "restart",
17
21
"displayName": "Disable Renderer Backgrounding",
18
22
"description": "This is enabled by default as a power saving measure, but it breaks screensharing and websocket connections fairly often",
19
23
"type": "boolean",
20
24
"default": true
21
25
},
22
26
"linuxAutoscroll": {
27
+
"advice": "restart",
23
28
"displayName": "Enable middle click autoscroll on Linux",
24
29
"description": "Requires manual configuration of your system to disable middle click paste, has no effect on other operating systems",
25
30
"type": "boolean",
26
31
"default": false
27
32
},
28
33
"linuxSpeechDispatcher": {
34
+
"advice": "restart",
29
35
"displayName": "Enable speech-dispatcher for TTS on Linux",
30
36
"description": "Fixes text-to-speech. Has no effect on other operating systems",
31
37
"type": "boolean",
32
38
"default": true
33
39
},
34
40
"vaapi": {
41
+
"advice": "restart",
35
42
"displayName": "Enable VAAPI features on Linux",
36
43
"description": "Provides hardware accelerated video encode and decode. Has no effect on other operating systems",
37
44
"type": "boolean",
+1
packages/core-extensions/src/noHideToken/manifest.json
+1
packages/core-extensions/src/noHideToken/manifest.json
+1
packages/core-extensions/src/noTrack/manifest.json
+1
packages/core-extensions/src/noTrack/manifest.json
+1
packages/core-extensions/src/notices/manifest.json
+1
packages/core-extensions/src/notices/manifest.json
+2
packages/core-extensions/src/quietLoggers/manifest.json
+2
packages/core-extensions/src/quietLoggers/manifest.json
···
1
1
{
2
+
"$schema": "https://moonlight-mod.github.io/manifest.schema.json",
2
3
"id": "quietLoggers",
3
4
"apiLevel": 2,
4
5
"meta": {
···
9
10
},
10
11
"settings": {
11
12
"xssDefensesOnly": {
13
+
"advice": "reload",
12
14
"displayName": "Only hide self-XSS",
13
15
"description": "Only disable self XSS prevention log",
14
16
"type": "boolean",
+2
packages/core-extensions/src/rocketship/manifest.json
+2
packages/core-extensions/src/rocketship/manifest.json
+1
packages/core-extensions/src/settings/manifest.json
+1
packages/core-extensions/src/settings/manifest.json
+2
packages/core-extensions/src/spacepack/manifest.json
+2
packages/core-extensions/src/spacepack/manifest.json
···
1
1
{
2
+
"$schema": "https://moonlight-mod.github.io/manifest.schema.json",
2
3
"id": "spacepack",
3
4
"apiLevel": 2,
4
5
"meta": {
···
9
10
},
10
11
"settings": {
11
12
"addToGlobalScope": {
13
+
"advice": "reload",
12
14
"displayName": "Add to global scope",
13
15
"description": "Populates window.spacepack for easier usage in DevTools",
14
16
"type": "boolean",
+1
-1
packages/core/src/patch.ts
+1
-1
packages/core/src/patch.ts
···
101
101
}
102
102
}
103
103
104
-
let modified = false;
105
104
for (const [id, func] of Object.entries(entry)) {
106
105
if (func.__moonlight === true) continue;
107
106
···
110
109
let moduleString = origModuleString;
111
110
const patchedStr = [];
112
111
const mappedName = moonlight.moonmap.modules[id];
112
+
let modified = false;
113
113
114
114
for (let i = 0; i < patches.length; i++) {
115
115
const patch = patches[i];
+2
-2
packages/injector/src/index.ts
+2
-2
packages/injector/src/index.ts
···
15
15
import persist from "@moonlight-mod/core/persist";
16
16
import createFS from "@moonlight-mod/core/fs";
17
17
import { getConfigOption, getManifest, setConfigOption } from "@moonlight-mod/core/util/config";
18
-
import { getExtensionsPath, getMoonlightDir } from "@moonlight-mod/core/util/data";
18
+
import { getConfigPath, getExtensionsPath, getMoonlightDir } from "@moonlight-mod/core/util/data";
19
19
20
20
const logger = new Logger("injector");
21
21
···
223
223
if (val == null || typeof val === "boolean") return undefined;
224
224
return val.config;
225
225
}
226
-
227
226
global.moonlightHost = {
228
227
get config() {
229
228
return config;
···
237
236
branch: MOONLIGHT_BRANCH as MoonlightBranch,
238
237
239
238
getConfig,
239
+
getConfigPath,
240
240
getConfigOption(ext, name) {
241
241
const manifest = getManifest(extensions, ext);
242
242
return getConfigOption(ext, name, config, manifest?.settings);
+7
packages/types/src/config.ts
+7
packages/types/src/config.ts
···
81
81
default?: any;
82
82
};
83
83
84
+
export enum ExtensionSettingsAdvice {
85
+
None = "none",
86
+
Reload = "reload",
87
+
Restart = "restart"
88
+
}
89
+
84
90
export type ExtensionSettingsManifest = {
85
91
displayName?: string;
86
92
description?: string;
93
+
advice?: ExtensionSettingsAdvice;
87
94
} & (
88
95
| BooleanSettingType
89
96
| NumberSettingType
+1
packages/types/src/globals.ts
+1
packages/types/src/globals.ts
···
18
18
branch: MoonlightBranch;
19
19
20
20
getConfig: (ext: string) => ConfigExtension["config"];
21
+
getConfigPath: () => Promise<string>;
21
22
getConfigOption: <T>(ext: string, name: string) => T | undefined;
22
23
setConfigOption: <T>(ext: string, name: string, value: T) => void;
23
24
writeConfig: (config: Config) => Promise<void>;
+13
-5
pnpm-lock.yaml
+13
-5
pnpm-lock.yaml
···
10
10
devDependencies:
11
11
'@moonlight-mod/eslint-config':
12
12
specifier: github:moonlight-mod/eslint-config
13
-
version: https://codeload.github.com/moonlight-mod/eslint-config/tar.gz/7eb7bd7c51fe0e3ee9d2a0baf149212d2bb893af(eslint@9.12.0)(prettier@3.1.0)(typescript@5.3.2)
13
+
version: https://codeload.github.com/moonlight-mod/eslint-config/tar.gz/e262ac24e1a0955a9b3e0d66da247a0a8c0446c9(eslint@9.12.0)(prettier@3.1.0)(typescript@5.3.2)
14
14
esbuild:
15
15
specifier: ^0.19.3
16
16
version: 0.19.3
···
62
62
'@moonlight-mod/types':
63
63
specifier: workspace:*
64
64
version: link:../types
65
+
microdiff:
66
+
specifier: ^1.5.0
67
+
version: 1.5.0
65
68
nanotar:
66
69
specifier: ^0.1.1
67
70
version: 0.1.1
···
311
314
resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
312
315
engines: {node: '>=18.18'}
313
316
314
-
'@moonlight-mod/eslint-config@https://codeload.github.com/moonlight-mod/eslint-config/tar.gz/7eb7bd7c51fe0e3ee9d2a0baf149212d2bb893af':
315
-
resolution: {tarball: https://codeload.github.com/moonlight-mod/eslint-config/tar.gz/7eb7bd7c51fe0e3ee9d2a0baf149212d2bb893af}
316
-
version: 1.0.0
317
+
'@moonlight-mod/eslint-config@https://codeload.github.com/moonlight-mod/eslint-config/tar.gz/e262ac24e1a0955a9b3e0d66da247a0a8c0446c9':
318
+
resolution: {tarball: https://codeload.github.com/moonlight-mod/eslint-config/tar.gz/e262ac24e1a0955a9b3e0d66da247a0a8c0446c9}
319
+
version: 1.0.1
317
320
peerDependencies:
318
321
eslint: '>= 9'
319
322
typescript: '>= 5.3'
···
1003
1006
meriyah@6.0.1:
1004
1007
resolution: {integrity: sha512-OyvYIOgpzXREySYJ1cqEb2pOKdeQMTfF9M8dRU6nC4hi/GXMmNpe9ssZCrSoTHazu05BSAoRBN/uYeco+ymfOg==}
1005
1008
engines: {node: '>=18.0.0'}
1009
+
1010
+
microdiff@1.5.0:
1011
+
resolution: {integrity: sha512-Drq+/THMvDdzRYrK0oxJmOKiC24ayUV8ahrt8l3oRK51PWt6gdtrIGrlIH3pT/lFh1z93FbAcidtsHcWbnRz8Q==}
1006
1012
1007
1013
micromatch@4.0.8:
1008
1014
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
···
1433
1439
1434
1440
'@humanwhocodes/retry@0.3.1': {}
1435
1441
1436
-
'@moonlight-mod/eslint-config@https://codeload.github.com/moonlight-mod/eslint-config/tar.gz/7eb7bd7c51fe0e3ee9d2a0baf149212d2bb893af(eslint@9.12.0)(prettier@3.1.0)(typescript@5.3.2)':
1442
+
'@moonlight-mod/eslint-config@https://codeload.github.com/moonlight-mod/eslint-config/tar.gz/e262ac24e1a0955a9b3e0d66da247a0a8c0446c9(eslint@9.12.0)(prettier@3.1.0)(typescript@5.3.2)':
1437
1443
dependencies:
1438
1444
'@eslint/js': 9.12.0
1439
1445
eslint: 9.12.0
···
2293
2299
merge2@1.4.1: {}
2294
2300
2295
2301
meriyah@6.0.1: {}
2302
+
2303
+
microdiff@1.5.0: {}
2296
2304
2297
2305
micromatch@4.0.8:
2298
2306
dependencies: