this repo has no description
1import { MoonlightBranch } from "@moonlight-mod/types";
2import type { MoonbaseNatives, RepositoryManifest } from "./types";
3import extractAsar from "@moonlight-mod/core/asar";
4import { distDir, repoUrlFile, installedVersionFile } from "@moonlight-mod/types/constants";
5import { parseTarGzip } from "nanotar";
6
7const moonlightGlobal = globalThis.moonlightHost ?? globalThis.moonlightNode;
8
9const githubRepo = "moonlight-mod/moonlight";
10const githubApiUrl = `https://api.github.com/repos/${githubRepo}/releases/latest`;
11const artifactName = "dist.tar.gz";
12
13const nightlyRefUrl = "https://moonlight-mod.github.io/moonlight/ref";
14const nightlyZipUrl = "https://moonlight-mod.github.io/moonlight/dist.tar.gz";
15
16export const userAgent = `moonlight/${moonlightGlobal.version} (https://github.com/moonlight-mod/moonlight)`;
17
18async function getStableRelease(): Promise<{
19 name: string;
20 assets: {
21 name: string;
22 browser_download_url: string;
23 }[];
24}> {
25 const req = await fetch(githubApiUrl, {
26 cache: "no-store",
27 headers: {
28 "User-Agent": userAgent
29 }
30 });
31 return await req.json();
32}
33
34export default function getNatives(): MoonbaseNatives {
35 const logger = moonlightGlobal.getLogger("moonbase/natives");
36
37 return {
38 async checkForMoonlightUpdate() {
39 try {
40 if (moonlightGlobal.branch === MoonlightBranch.STABLE) {
41 const json = await getStableRelease();
42 return json.name !== moonlightGlobal.version ? json.name : null;
43 } else if (moonlightGlobal.branch === MoonlightBranch.NIGHTLY) {
44 const req = await fetch(nightlyRefUrl, {
45 cache: "no-store",
46 headers: {
47 "User-Agent": userAgent
48 }
49 });
50 const ref = (await req.text()).split("\n")[0];
51 return ref !== moonlightGlobal.version ? ref : null;
52 }
53
54 return null;
55 } catch (e) {
56 logger.error("Error checking for moonlight update", e);
57 return null;
58 }
59 },
60
61 async updateMoonlight(overrideBranch?: MoonlightBranch) {
62 const branch = overrideBranch ?? moonlightGlobal.branch;
63
64 // Note: this won't do anything on browser, we should probably disable it
65 // entirely when running in browser.
66 async function downloadStable(): Promise<[ArrayBuffer, string]> {
67 const json = await getStableRelease();
68 const asset = json.assets.find((a) => a.name === artifactName);
69 if (!asset) throw new Error("Artifact not found");
70
71 logger.debug(`Downloading ${asset.browser_download_url}`);
72 const req = await fetch(asset.browser_download_url, {
73 cache: "no-store",
74 headers: {
75 "User-Agent": userAgent
76 }
77 });
78
79 return [await req.arrayBuffer(), json.name];
80 }
81
82 async function downloadNightly(): Promise<[ArrayBuffer, string]> {
83 logger.debug(`Downloading ${nightlyZipUrl}`);
84 const zipReq = await fetch(nightlyZipUrl, {
85 cache: "no-store",
86 headers: {
87 "User-Agent": userAgent
88 }
89 });
90
91 const refReq = await fetch(nightlyRefUrl, {
92 cache: "no-store",
93 headers: {
94 "User-Agent": userAgent
95 }
96 });
97 const ref = (await refReq.text()).split("\n")[0];
98
99 return [await zipReq.arrayBuffer(), ref];
100 }
101
102 const [tar, ref] =
103 branch === MoonlightBranch.STABLE
104 ? await downloadStable()
105 : branch === MoonlightBranch.NIGHTLY
106 ? await downloadNightly()
107 : [null, null];
108
109 if (!tar || !ref) return;
110
111 const dist = moonlightNodeSandboxed.fs.join(moonlightGlobal.getMoonlightDir(), distDir);
112 if (await moonlightNodeSandboxed.fs.exists(dist)) await moonlightNodeSandboxed.fs.rmdir(dist);
113 await moonlightNodeSandboxed.fs.mkdir(dist);
114
115 logger.debug("Extracting update");
116 const files = await parseTarGzip(tar);
117 for (const file of files) {
118 if (!file.data) continue;
119 // @ts-expect-error What do you mean their own types are wrong
120 if (file.type !== "file") continue;
121
122 const fullFile = moonlightNodeSandboxed.fs.join(dist, file.name);
123 const fullDir = moonlightNodeSandboxed.fs.dirname(fullFile);
124 if (!(await moonlightNodeSandboxed.fs.exists(fullDir))) await moonlightNodeSandboxed.fs.mkdir(fullDir);
125 await moonlightNodeSandboxed.fs.writeFile(fullFile, file.data);
126 }
127
128 logger.debug("Writing version file:", ref);
129 const versionFile = moonlightNodeSandboxed.fs.join(moonlightGlobal.getMoonlightDir(), installedVersionFile);
130 await moonlightNodeSandboxed.fs.writeFileString(versionFile, ref.trim());
131
132 logger.debug("Update extracted");
133 },
134
135 async fetchRepositories(repos) {
136 const ret: Record<string, RepositoryManifest[]> = {};
137
138 for (const repo of repos) {
139 try {
140 const req = await fetch(repo, {
141 cache: "no-store",
142 headers: {
143 "User-Agent": userAgent
144 }
145 });
146 const json = await req.json();
147 ret[repo] = json;
148 } catch (e) {
149 logger.error(`Error fetching repository ${repo}`, e);
150 }
151 }
152
153 return ret;
154 },
155
156 async installExtension(manifest, url, repo) {
157 const req = await fetch(url, {
158 cache: "no-store",
159 headers: {
160 "User-Agent": userAgent
161 }
162 });
163
164 const dir = moonlightGlobal.getExtensionDir(manifest.id);
165 // remake it in case of updates
166 if (await moonlightNodeSandboxed.fs.exists(dir)) await moonlightNodeSandboxed.fs.rmdir(dir);
167 await moonlightNodeSandboxed.fs.mkdir(dir);
168
169 const buffer = await req.arrayBuffer();
170 const files = extractAsar(buffer);
171 for (const [file, buf] of Object.entries(files)) {
172 const fullFile = moonlightNodeSandboxed.fs.join(dir, file);
173 const fullDir = moonlightNodeSandboxed.fs.dirname(fullFile);
174
175 if (!(await moonlightNodeSandboxed.fs.exists(fullDir))) await moonlightNodeSandboxed.fs.mkdir(fullDir);
176 await moonlightNodeSandboxed.fs.writeFile(moonlightNodeSandboxed.fs.join(dir, file), buf);
177 }
178
179 await moonlightNodeSandboxed.fs.writeFileString(moonlightNodeSandboxed.fs.join(dir, repoUrlFile), repo);
180 },
181
182 async deleteExtension(id) {
183 const dir = moonlightGlobal.getExtensionDir(id);
184 await moonlightNodeSandboxed.fs.rmdir(dir);
185 }
186 };
187}