import {
writeFileSync,
unlinkSync,
mkdirSync,
readdirSync,
lstatSync,
copyFileSync,
rmSync,
existsSync,
} from 'node:fs';
import { dirname, join } from 'node:path';
import { exec, fork } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { build } from 'esbuild';
import {
config,
serverUrl,
flatAltPaths,
splashRandom,
} from './src/routes.mjs';
import { epoxyPath } from '@mercuryworkshop/epoxy-transport';
import { libcurlPath } from '@mercuryworkshop/libcurl-transport';
import { baremuxPath } from '@mercuryworkshop/bare-mux/node';
import { uvPath } from '@titaniumnetwork-dev/ultraviolet';
import paintSource from './src/source-rewrites.mjs';
import { loadTemplates, tryReadFile } from './src/templates.mjs';
const scramjetPath = join(
dirname(fileURLToPath(import.meta.url)),
'node_modules/@mercuryworkshop/scramjet/dist'
);
const shutdown = fileURLToPath(new URL('./src/.shutdown', import.meta.url));
commands: for (let i = 2; i < process.argv.length; i++)
switch (process.argv[i]) {
case 'start':
if (config.production)
exec(
'npx pm2 start ecosystem.config.js --env production',
(error, stdout) => {
if (error) throw error;
console.log('[Start]', stdout);
}
);
else if (process.platform === 'win32')
exec('START /MIN "" node backend.js', (error, stdout) => {
if (error) {
console.error('[Start Error]', error);
process.exitCode = 1;
}
console.log('[Start]', stdout);
});
else {
const server = fork(
fileURLToPath(new URL('./backend.js', import.meta.url)),
{ cwd: process.cwd(), detached: true }
);
server.unref();
server.disconnect();
}
break;
case 'stop': {
writeFileSync(shutdown, '');
let timeoutId,
hasErrored = false;
try {
const response = await Promise.race([
fetch(new URL(serverUrl.pathname + 'test-shutdown', serverUrl)),
new Promise((resolve) => {
timeoutId = setTimeout(() => {
resolve('Error');
}, 5000);
}),
]);
clearTimeout(timeoutId);
if (response === 'Error') throw new Error('Server is unresponsive.');
} catch (e) {
unlinkSync(shutdown);
if (e instanceof TypeError) clearTimeout(timeoutId);
else {
console.error('[Stop Error]', e);
if (!process.argv.slice(i + 1).includes('kill')) hasErrored = true;
}
}
if (config.production && !process.argv.slice(i + 1).includes('kill'))
exec('npx pm2 stop ecosystem.config.js', (error, stdout) => {
if (error) {
console.error('[Stop Error]', error);
hasErrored = true;
}
console.log('[Stop]', stdout);
});
if (hasErrored) {
process.exitCode = 1;
break commands;
}
break;
}
case 'build': {
const dist = fileURLToPath(new URL('./views/dist', import.meta.url));
rmSync(dist, { force: true, recursive: true });
mkdirSync(dist);
const ignoredDirectories = ['dist', 'assets', 'uv', 'scram', 'archive'];
const ignoredFileTypes = /\.map$/;
const compile = (
dir,
base = '',
outDir = '',
initialDir = dir,
applyRewrites = initialDir === './views'
) =>
readdirSync(base + dir).forEach((file) => {
let oldLocation = new URL(
file,
new URL(base + dir + '/', import.meta.url)
);
if (
(ignoredDirectories.includes(file) && applyRewrites) ||
ignoredFileTypes.test(file)
)
return;
const fileStats = lstatSync(oldLocation),
targetPath = fileURLToPath(
new URL(
'./views/dist/' +
outDir +
(base + dir + '/').slice(initialDir.length + 1) +
((!config.usingSEO && flatAltPaths['files/' + file]) || file),
import.meta.url
)
);
if (fileStats.isFile() && !existsSync(targetPath))
if (/\.(?:html|js|css|json|txt|xml)$/.test(file) && applyRewrites)
writeFileSync(
targetPath,
paintSource(
loadTemplates(
tryReadFile(base + dir + '/' + file, import.meta.url, false)
)
)
);
else copyFileSync(base + dir + '/' + file, targetPath);
else if (fileStats.isDirectory()) {
if (!existsSync(targetPath)) mkdirSync(targetPath);
compile(file, base + dir + '/', outDir, initialDir, applyRewrites);
}
});
const localAssetDirs = ['assets', 'scram', 'uv'];
for (const path of localAssetDirs) {
mkdirSync('./views/dist/' + path);
compile('./views/' + path, '', path + '/', './views/' + path, true);
}
const compilePaths = {
epoxy: epoxyPath,
libcurl: libcurlPath,
baremux: baremuxPath,
uv: uvPath,
scram: scramjetPath,
chii: 'node_modules/chii',
};
for (const path of Object.entries(compilePaths)) {
const prefix = path[0] + '/',
prefixUrl = new URL('./views/dist/' + prefix, import.meta.url);
if (!existsSync(prefixUrl)) mkdirSync(prefixUrl);
compile(path[1].slice(path[1].indexOf('node_modules')), '', prefix);
}
if (config.minifyScripts)
await build({
entryPoints: [
'./views/dist/uv/**/*.js',
'./views/dist/scram/**/*.js',
'./views/dist/scram/**/*.wasm.wasm',
'./views/dist/assets/js/**/*.js',
'./views/dist/assets/css/**/*.css',
],
platform: 'browser',
sourcemap: true,
bundle: true,
minify: true,
loader: { '.wasm.wasm': 'copy' },
external: ['*.png', '*.jpg', '*.jpeg', '*.webp', '*.svg'],
outdir: dist,
allowOverwrite: true,
});
compile('./views');
mkdirSync('./views/dist/archive');
if (existsSync('./views/archive'))
compile('./views/archive', '', 'archive/');
const createFile = (location, text) => {
writeFileSync(
fileURLToPath(new URL('./views/dist/' + location, import.meta.url)),
paintSource(loadTemplates(text))
);
};
createFile('assets/json/splash.json', JSON.stringify(splashRandom));
if (config.disguiseFiles) {
const compress = async (dir, recursive = false) => {
for (const file of readdirSync(dir)) {
const fileLocation = dir + '/' + file;
if (file.endsWith('.html'))
writeFileSync(
fileLocation,
Buffer.from(
await new Response(
new Blob([
tryReadFile(fileLocation, import.meta.url, false),
])
.stream()
.pipeThrough(new CompressionStream('gzip'))
).arrayBuffer()
)
);
else if (
recursive &&
lstatSync(fileLocation).isDirectory() &&
file !== 'deobf'
)
await compress(fileLocation, true);
}
};
await compress('./views/dist');
await compress('./views/dist/pages', true);
await compress('./views/dist/archive', true);
}
break;
}
case 'clean': {
const targetDirs = ['./lib/rammerhead/cache-js'];
for (const targetDir of targetDirs)
try {
const targetPath = fileURLToPath(new URL(targetDir, import.meta.url));
rmSync(targetPath, { force: true, recursive: true });
mkdirSync(targetPath);
writeFileSync(
fileURLToPath(new URL(targetDir + '/.gitkeep', import.meta.url)),
''
);
console.log(
'[Clean]',
`Reset folder ${targetDir} at ${new Date().toISOString()}.`
);
} catch (e) {
console.error('[Clean Error]', e);
}
break;
}
case 'format': {
exec('npx prettier --write .', (error, stdout) => {
if (error) {
console.error('[Clean Error]', error);
}
console.log('[Clean]', stdout);
});
break;
}
case 'kill':
if (process.platform === 'win32')
exec(
'( npx pm2 delete ecosystem.config.js ) ; taskkill /F /IM node*',
(error, stdout) => {
console.log('[Kill]', stdout);
}
);
else
exec(
'npx pm2 delete ecosystem.config.js; pkill node',
(error, stdout) => {
console.log('[Kill]', stdout);
}
);
break;
case 'workflow': {
const tempServer = fork(
fileURLToPath(new URL('./backend.js', import.meta.url)),
{
cwd: process.cwd(),
stdio: ['inherit', 'pipe', 'pipe', 'ipc'],
detached: true,
}
);
tempServer.stderr.on('data', (stderr) => {
if (stderr.toString().indexOf('DeprecationWarning') >= 0)
return console.log(stderr.toString());
console.error(stderr.toString());
tempServer.kill();
process.exitCode = 1;
});
tempServer.stdout.on('data', () => {
tempServer.kill();
const server = fork(
fileURLToPath(new URL('./backend.js', import.meta.url)),
{ cwd: process.cwd(), stdio: 'ignore', detached: true }
);
server.unref();
server.disconnect();
});
tempServer.unref();
tempServer.disconnect();
break;
}
}
process.exitCode = process.exitCode || 0;