-
Notifications
You must be signed in to change notification settings - Fork 2
/
utils.ts
171 lines (160 loc) · 6.05 KB
/
utils.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
import { createWriteStream } from 'node:fs';
import { exists, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { Readable } from 'node:stream';
import type { ReadableStream } from 'node:stream/web';
import { promisify } from 'node:util';
import { log, spinner } from '@clack/prompts';
import asar from '@electron/asar';
import AdmZip from 'adm-zip';
import findProcess from 'find-process';
import versionInfo from 'win-version-info';
const NODEJS_DIST_URL = 'https://nodejs.org/dist';
const DEFAULT_TIDAL_PATH = join(import.meta.env.APPDATA ?? '', '../Local/TIDAL');
const EXECUTABLE_NAME = 'TIDAL.exe';
export const tidalPath = DEFAULT_TIDAL_PATH;
export function isWindowsPlatform() {
const isWindows = process.platform === 'win32';
if (!isWindows) log.error('Only Windows platforms are supported');
return isWindows;
}
export async function isAppRunning() {
const s = spinner();
s.start('Checking if TIDAL is running...');
const programs = await findProcess('name', EXECUTABLE_NAME);
const isRunning = !!programs.length;
if (isRunning) s.stop('Close the app before running the patcher!', 2);
else s.stop('TIDAL is not running');
return isRunning;
}
export async function existsInDefaultPath() {
const fileExists = await exists(join(tidalPath, EXECUTABLE_NAME));
if (fileExists) log.info(`Executable found in default path: ${tidalPath}`);
else log.error('Executable not found');
return exists;
}
export async function getAppDirName() {
let appVersionDirName: string | undefined;
try {
const appVersion = versionInfo(join(tidalPath, EXECUTABLE_NAME)).FileVersion;
if (appVersion) appVersionDirName = `app-${appVersion.split('.').slice(0, 3).join('.')}`;
else {
const appDirName = await readdir(tidalPath, { withFileTypes: true });
appVersionDirName = appDirName
.filter((dirent) => dirent.isDirectory())
.find((dirent) => dirent.name.startsWith('app'))?.name;
}
if (appVersionDirName && (await exists(join(tidalPath, appVersionDirName)))) {
log.info(`App directory: ${appVersionDirName}`);
return appVersionDirName;
}
log.error('App directory not found');
} catch (error) {
log.error('Error looking for app directory');
log.error((error as Error).message);
}
}
export async function waitForTimeout(timeout = 100) {
return new Promise((resolve) => setTimeout(resolve, timeout));
}
export async function extractSourceFiles(asarFilePath: string, sourcePath: string) {
const s = spinner();
s.start('Extracting source files...');
try {
await rm(sourcePath, { recursive: true, force: true });
// ensures that the spinner appears, although it will get stuck because asar.extractAll()
// is synchronous
await waitForTimeout();
asar.extractAll(asarFilePath, sourcePath);
s.stop('Source files extracted');
} catch (error) {
s.stop('Error extracting source files', 2);
throw error;
}
}
export type Modifications = {
reference: RegExp;
code: string;
type?: 'replace' | 'newLine' | 'exact';
offset?: number; // only applicable if type is 'newLine'
};
export async function injectCode(filePath: string, modifications: Modifications[]) {
const fileName = filePath.split('\\').at(-1);
let file: string | undefined;
try {
file = await readFile(filePath, { encoding: 'utf8' });
if (!file) throw new Error(`File ${fileName} not found`);
} catch (error) {
log.error((error as Error).message);
return;
}
let modifiedFile = file;
try {
for (const { reference, code, type = 'newLine', offset = 0 } of modifications) {
if (!reference.test(file)) {
log.warn(
`Reference \`${reference}\` not found in file ${fileName}. Skipping modification...`,
);
continue;
}
if (type === 'replace') modifiedFile = modifiedFile.replace(reference, code);
else if (type === 'newLine') {
const lines = modifiedFile.split(/\r?\n/);
const lineIndex = lines.findIndex((line) => line.match(reference));
lines.splice(lineIndex + 1 + offset, 0, code);
modifiedFile = lines.join('\n');
} else if (type === 'exact') {
const charIndex = modifiedFile.search(reference);
modifiedFile = modifiedFile.slice(0, charIndex) + code + modifiedFile.slice(charIndex);
}
}
await writeFile(filePath, modifiedFile);
} catch (error) {
log.error(`Error while modifying file ${fileName}`);
throw error;
}
}
export async function download(url: string, outputPath: string) {
const response = await fetch(url);
if (response.ok && response.body) {
const writeStream = createWriteStream(outputPath);
// type error: https://github.com/DefinitelyTyped/DefinitelyTyped/discussions/65542#discussioncomment-6071004
const body = response.body as unknown as ReadableStream<Uint8Array>;
Readable.fromWeb(body).pipe(writeStream);
return new Promise((resolve, reject) => {
writeStream.on('error', reject);
writeStream.on('close', resolve);
});
}
}
export async function downloadNpm(outputPath = 'node') {
let latestVersion: string | undefined;
const s = spinner();
s.start('Downloading npm from nodejs.org...');
try {
const response = await fetch(`${NODEJS_DIST_URL}/index.json`);
const versions = await response.json();
latestVersion = versions[0].version;
const downloadURL = `${NODEJS_DIST_URL}/${latestVersion}/node-${latestVersion}-win-x64.zip`;
await rm('node.zip', { force: true });
await download(downloadURL, 'node.zip');
s.stop('npm downloaded');
} catch (error) {
s.stop('Error downloading npm', 2);
throw error;
}
const s2 = spinner();
s2.start('Extracting npm...');
try {
const zip = new AdmZip('node.zip');
const extractAllTo = promisify(zip.extractAllToAsync.bind(zip));
await extractAllTo('', true, false);
await rm(outputPath, { force: true });
await rename(`node-${latestVersion}-win-x64`, outputPath);
await rm('node.zip');
s2.stop('npm extracted');
} catch (error) {
s2.stop('Error extracting npm', 2);
throw error;
}
}