Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
QuiteAFancyEmerald
GitHub Repository: QuiteAFancyEmerald/Holy-Unblocker
Path: blob/master/run-command.mjs
5154 views
1
import {
2
writeFileSync,
3
unlinkSync,
4
mkdirSync,
5
readdirSync,
6
lstatSync,
7
copyFileSync,
8
rmSync,
9
existsSync,
10
} from 'node:fs';
11
import { dirname, join } from 'node:path';
12
import { exec, fork } from 'node:child_process';
13
import { fileURLToPath } from 'node:url';
14
import { build } from 'esbuild';
15
import {
16
config,
17
serverUrl,
18
flatAltPaths,
19
splashRandom,
20
} from './src/routes.mjs';
21
import { epoxyPath } from '@mercuryworkshop/epoxy-transport';
22
import { libcurlPath } from '@mercuryworkshop/libcurl-transport';
23
import { baremuxPath } from '@mercuryworkshop/bare-mux/node';
24
import { uvPath } from '@titaniumnetwork-dev/ultraviolet';
25
import paintSource from './src/source-rewrites.mjs';
26
import { loadTemplates, tryReadFile } from './src/templates.mjs';
27
28
const scramjetPath = join(
29
dirname(fileURLToPath(import.meta.url)),
30
'node_modules/@mercuryworkshop/scramjet/dist'
31
);
32
33
// This constant is copied over from /src/server.mjs.
34
const shutdown = fileURLToPath(new URL('./src/.shutdown', import.meta.url));
35
36
// Run each command line argument passed after node run-command.mjs.
37
// Commands are defined in the switch case statement below.
38
commands: for (let i = 2; i < process.argv.length; i++)
39
switch (process.argv[i]) {
40
// Commmand to boot up the server. Use PM2 to run if production is true in the
41
// config file.
42
case 'start':
43
if (config.production)
44
exec(
45
'npx pm2 start ecosystem.config.js --env production',
46
(error, stdout) => {
47
if (error) throw error;
48
console.log('[Start]', stdout);
49
}
50
);
51
// Handle setup on Windows differently from platforms with POSIX-compliant
52
// shells. This should run the server as a background process.
53
else if (process.platform === 'win32')
54
exec('START /MIN "" node backend.js', (error, stdout) => {
55
if (error) {
56
console.error('[Start Error]', error);
57
process.exitCode = 1;
58
}
59
console.log('[Start]', stdout);
60
});
61
// The following approach (and similar approaches) will not work on Windows,
62
// because exiting this program will also terminate backend.js on Windows.
63
else {
64
const server = fork(
65
fileURLToPath(new URL('./backend.js', import.meta.url)),
66
{ cwd: process.cwd(), detached: true }
67
);
68
server.unref();
69
server.disconnect();
70
}
71
break;
72
73
// Stop the server. Make a temporary file that the server will check for if told
74
// to shut down. This is done by sending a GET request to the server.
75
case 'stop': {
76
writeFileSync(shutdown, '');
77
let timeoutId,
78
hasErrored = false;
79
try {
80
/* Give the server 5 seconds to respond, otherwise cancel this and throw an
81
* error to the console. The fetch request will also throw an error
82
* immediately if checking the server on localhost and the port is unused.
83
*/
84
const response = await Promise.race([
85
fetch(new URL(serverUrl.pathname + 'test-shutdown', serverUrl)),
86
new Promise((resolve) => {
87
timeoutId = setTimeout(() => {
88
resolve('Error');
89
}, 5000);
90
}),
91
]);
92
clearTimeout(timeoutId);
93
if (response === 'Error') throw new Error('Server is unresponsive.');
94
} catch (e) {
95
// Remove the temporary shutdown file since the server didn't remove it.
96
unlinkSync(shutdown);
97
// Check if this is the error thrown by the fetch request for an unused port.
98
// Don't print the unused port error, since nothing has actually broken.
99
if (e instanceof TypeError) clearTimeout(timeoutId);
100
else {
101
console.error('[Stop Error]', e);
102
// Stop here unless Node will be killed later.
103
if (!process.argv.slice(i + 1).includes('kill')) hasErrored = true;
104
}
105
}
106
// Do not run this if Node will be killed later in this script. It will fail.
107
if (config.production && !process.argv.slice(i + 1).includes('kill'))
108
exec('npx pm2 stop ecosystem.config.js', (error, stdout) => {
109
if (error) {
110
console.error('[Stop Error]', error);
111
hasErrored = true;
112
}
113
console.log('[Stop]', stdout);
114
});
115
// Do not continue executing commands since the server was unable to be stopped.
116
// Mostly implemented to prevent duplicating Node instances with npm restart.
117
if (hasErrored) {
118
process.exitCode = 1;
119
break commands;
120
}
121
break;
122
}
123
124
case 'build': {
125
const dist = fileURLToPath(new URL('./views/dist', import.meta.url));
126
rmSync(dist, { force: true, recursive: true });
127
mkdirSync(dist);
128
129
/* The archive directory is excluded from this process, since source
130
* rewrites are not intended to be used by any of those files.
131
* Assets are compiled separately, before the rest of the files.
132
*/
133
const ignoredDirectories = ['dist', 'assets', 'uv', 'scram', 'archive'];
134
const ignoredFileTypes = /\.map$/;
135
136
const compile = (
137
dir,
138
base = '',
139
outDir = '',
140
initialDir = dir,
141
applyRewrites = initialDir === './views'
142
) =>
143
readdirSync(base + dir).forEach((file) => {
144
let oldLocation = new URL(
145
file,
146
new URL(base + dir + '/', import.meta.url)
147
);
148
if (
149
(ignoredDirectories.includes(file) && applyRewrites) ||
150
ignoredFileTypes.test(file)
151
)
152
return;
153
const fileStats = lstatSync(oldLocation),
154
targetPath = fileURLToPath(
155
new URL(
156
'./views/dist/' +
157
outDir +
158
(base + dir + '/').slice(initialDir.length + 1) +
159
((!config.usingSEO && flatAltPaths['files/' + file]) || file),
160
import.meta.url
161
)
162
);
163
if (fileStats.isFile() && !existsSync(targetPath))
164
if (/\.(?:html|js|css|json|txt|xml)$/.test(file) && applyRewrites)
165
writeFileSync(
166
targetPath,
167
paintSource(
168
loadTemplates(
169
tryReadFile(base + dir + '/' + file, import.meta.url, false)
170
)
171
)
172
);
173
else copyFileSync(base + dir + '/' + file, targetPath);
174
else if (fileStats.isDirectory()) {
175
if (!existsSync(targetPath)) mkdirSync(targetPath);
176
compile(file, base + dir + '/', outDir, initialDir, applyRewrites);
177
}
178
});
179
180
const localAssetDirs = ['assets', 'scram', 'uv'];
181
for (const path of localAssetDirs) {
182
mkdirSync('./views/dist/' + path);
183
compile('./views/' + path, '', path + '/', './views/' + path, true);
184
}
185
186
// Combine scripts from the corresponding node modules into the same
187
// dist-generated directories for compiling, and avoid overwriting files.
188
const compilePaths = {
189
epoxy: epoxyPath,
190
libcurl: libcurlPath,
191
baremux: baremuxPath,
192
uv: uvPath,
193
scram: scramjetPath,
194
chii: 'node_modules/chii',
195
};
196
for (const path of Object.entries(compilePaths)) {
197
const prefix = path[0] + '/',
198
prefixUrl = new URL('./views/dist/' + prefix, import.meta.url);
199
if (!existsSync(prefixUrl)) mkdirSync(prefixUrl);
200
201
compile(path[1].slice(path[1].indexOf('node_modules')), '', prefix);
202
}
203
204
// Minify the scripts and stylesheets upon compiling, if enabled in config.
205
if (config.minifyScripts)
206
await build({
207
entryPoints: [
208
'./views/dist/uv/**/*.js',
209
'./views/dist/scram/**/*.js',
210
'./views/dist/scram/**/*.wasm.wasm',
211
'./views/dist/assets/js/**/*.js',
212
'./views/dist/assets/css/**/*.css',
213
],
214
platform: 'browser',
215
sourcemap: true,
216
bundle: true,
217
minify: true,
218
loader: { '.wasm.wasm': 'copy' },
219
external: ['*.png', '*.jpg', '*.jpeg', '*.webp', '*.svg'],
220
outdir: dist,
221
allowOverwrite: true,
222
});
223
224
compile('./views');
225
226
// Compile the archive directory separately.
227
mkdirSync('./views/dist/archive');
228
if (existsSync('./views/archive'))
229
compile('./views/archive', '', 'archive/');
230
231
const createFile = (location, text) => {
232
writeFileSync(
233
fileURLToPath(new URL('./views/dist/' + location, import.meta.url)),
234
paintSource(loadTemplates(text))
235
);
236
};
237
238
createFile('assets/json/splash.json', JSON.stringify(splashRandom));
239
240
if (config.disguiseFiles) {
241
const compress = async (dir, recursive = false) => {
242
for (const file of readdirSync(dir)) {
243
const fileLocation = dir + '/' + file;
244
if (file.endsWith('.html'))
245
writeFileSync(
246
fileLocation,
247
Buffer.from(
248
await new Response(
249
new Blob([
250
tryReadFile(fileLocation, import.meta.url, false),
251
])
252
.stream()
253
.pipeThrough(new CompressionStream('gzip'))
254
).arrayBuffer()
255
)
256
);
257
else if (
258
recursive &&
259
lstatSync(fileLocation).isDirectory() &&
260
file !== 'deobf'
261
)
262
await compress(fileLocation, true);
263
}
264
};
265
await compress('./views/dist');
266
await compress('./views/dist/pages', true);
267
await compress('./views/dist/archive', true);
268
}
269
270
break;
271
}
272
273
// Delete all files in target locations. This is primarily used to manage
274
// Rammerhead's cache output.
275
case 'clean': {
276
// If including Rammerhead sessions, be careful to not let the global
277
// autocomplete session be deleted without restarting the server.
278
const targetDirs = ['./lib/rammerhead/cache-js'];
279
for (const targetDir of targetDirs)
280
try {
281
const targetPath = fileURLToPath(new URL(targetDir, import.meta.url));
282
rmSync(targetPath, { force: true, recursive: true });
283
mkdirSync(targetPath);
284
writeFileSync(
285
fileURLToPath(new URL(targetDir + '/.gitkeep', import.meta.url)),
286
''
287
);
288
console.log(
289
'[Clean]',
290
`Reset folder ${targetDir} at ${new Date().toISOString()}.`
291
);
292
} catch (e) {
293
console.error('[Clean Error]', e);
294
}
295
break;
296
}
297
298
case 'format': {
299
exec('npx prettier --write .', (error, stdout) => {
300
if (error) {
301
console.error('[Clean Error]', error);
302
}
303
console.log('[Clean]', stdout);
304
});
305
break;
306
}
307
308
/* Kill all node processes and fully reset PM2. To be used for debugging.
309
* Using npx pm2 monit, or npx pm2 list in the terminal will also bring up
310
* more PM2 debugging tools.
311
*/
312
case 'kill':
313
if (process.platform === 'win32')
314
exec(
315
'( npx pm2 delete ecosystem.config.js ) ; taskkill /F /IM node*',
316
(error, stdout) => {
317
console.log('[Kill]', stdout);
318
}
319
);
320
else
321
exec(
322
'npx pm2 delete ecosystem.config.js; pkill node',
323
(error, stdout) => {
324
console.log('[Kill]', stdout);
325
}
326
);
327
break;
328
329
/* Make a temporary server solely to test startup errors. The server will
330
* stop the command if there is an error, and restart itself otherwise.
331
* This uses the same command for both Windows and other platforms, but
332
* consequently forces the server to stay completely silent after startup.
333
*/
334
case 'workflow': {
335
const tempServer = fork(
336
fileURLToPath(new URL('./backend.js', import.meta.url)),
337
{
338
cwd: process.cwd(),
339
stdio: ['inherit', 'pipe', 'pipe', 'ipc'],
340
detached: true,
341
}
342
);
343
tempServer.stderr.on('data', (stderr) => {
344
// The temporary server will print startup errors that aren't deprecation
345
// warnings; stop the process and return an error exit code upon doing so.
346
if (stderr.toString().indexOf('DeprecationWarning') >= 0)
347
return console.log(stderr.toString());
348
console.error(stderr.toString());
349
tempServer.kill();
350
process.exitCode = 1;
351
});
352
tempServer.stdout.on('data', () => {
353
// There are no startup errors by this point, so kill the server and start
354
// over. The restart alters stdio to prevent the workflow check from hanging.
355
tempServer.kill();
356
const server = fork(
357
fileURLToPath(new URL('./backend.js', import.meta.url)),
358
// The stdio: 'ignore' makes the server completely silent, yet it is also
359
// why this works for Windows when the start command's version does not.
360
{ cwd: process.cwd(), stdio: 'ignore', detached: true }
361
);
362
server.unref();
363
server.disconnect();
364
});
365
tempServer.unref();
366
tempServer.disconnect();
367
break;
368
}
369
370
// No default case.
371
}
372
373
process.exitCode = process.exitCode || 0;
374
375