rjweb-server
Version:
Easy and Robust Way to create a Web Server with Many Easy-to-use Features in NodeJS
345 lines (344 loc) • 12.1 kB
JavaScript
import { colors } from "./classes/Logger";
import { Server, defaultOptions, version } from "./index";
import { exec, execSync } from "child_process";
import https from "https";
import yargs from "yargs";
import pPath from "path/posix";
import path from "path";
import fs from "fs";
class Spinner {
constructor() {
this.current = 0;
this.spinner = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
}
get() {
this.current = (this.current + 1) % this.spinner.length;
return this.spinner[this.current];
}
}
const prefix = `⚡ ${colors.fg.white}[RJWEB ${version.split('.')[0]}]${colors.fg.gray}:${colors.reset}`;
const getAllOptionKeys = (options) => {
return Object.keys(options).map((key) => {
if (typeof options[key] === 'object' && !Array.isArray(options[key]))
return getAllOptionKeys(options[key]).map((k) => `${key}.${k}`);
else
return key;
}).flat();
};
const resolveOptionKey = (key) => {
const parts = key.split('.');
let options = defaultOptions;
for (const part of parts) {
options = options[part];
}
return options;
};
const optionKeyValueToObject = (keys) => {
let output = {};
for (const key in keys) {
const parts = key.split('.');
let current = output;
for (let i = 0; i < parts.length; i++) {
if (i === parts.length - 1) {
current[parts[i]] = keys[key];
}
else {
if (!current[parts[i]])
current[parts[i]] = {};
current = current[parts[i]];
}
}
}
return output;
};
const coloredPath = (path) => {
let output = [];
for (const part of path.split('/')) {
output.push(`${colors.fg.blue}${part}`);
}
return output.join(`${colors.fg.cyan}/`);
};
const pR = (location) => {
return path.join(process.cwd(), location);
};
const isX = (type, path) => {
let infos;
try {
infos = fs.statSync(path);
}
catch {
return false;
}
if (type === 'dir')
return infos.isDirectory();
else
return infos.isFile();
};
yargs
.scriptName('rjweb')
.usage('$0 <command> [args]')
.version(version)
.command('serve <folder>', 'Serve a Folder',
// @ts-ignore
((cmd) => {
cmd
.positional('folder', {
type: 'string',
description: 'The Folder to serve',
demandOption: true
})
.option('runtime', {
type: 'string',
description: 'The Runtime Package to use',
alias: ['r'],
default: '@rjweb/runtime-node',
demandOption: true
})
.option('stripHtmlEnding', {
type: 'boolean',
description: 'Strip the .html Ending from Files',
alias: ['s'],
default: true
});
for (const key of getAllOptionKeys(defaultOptions)) {
if (key === 'version')
continue;
cmd.option(key, {
description: `The ${key} Option`,
type: typeof resolveOptionKey(key) === 'object' ? 'array' : typeof resolveOptionKey(key)
});
}
return cmd;
}), (async (args) => {
if (!isX('dir', pR(args.folder ?? '//')))
return console.error(`${prefix} ${colors.fg.red}Could not find ${colors.fg.cyan}${args.folder}`);
const serverOptions = getAllOptionKeys(defaultOptions).reduce((acc, key) => {
acc[key] = args[key] ?? resolveOptionKey(key);
return acc;
}, {});
console.log(`${prefix} ${colors.fg.gray}Starting Server...`);
const server = new Server(await import(args.runtime).then((runtime) => runtime.Runtime).catch(() => {
console.error(`${prefix} ${colors.fg.red}Could not find Runtime Package ${colors.fg.cyan}${args.runtime} ${colors.fg.red}installed globally.`);
process.exit(1);
}), optionKeyValueToObject(serverOptions));
server.path('/', (path) => path
.static(pR(args.folder), {
stripHtmlEnding: args.stripHtmlEnding
}));
server.http((ctr) => {
console.log(`${prefix} ${colors.fg.gray}${ctr.client.ip.usual()} ${colors.fg.green}HTTP ${ctr.url.method} ${colors.fg.cyan}${coloredPath(ctr.url.path)}`);
});
server.start()
.then((port) => {
console.log(`${prefix} ${colors.fg.green}Started on Port ${colors.fg.cyan}${port}`);
})
.catch((err) => {
console.error(`${prefix} ${colors.fg.red}An Error occured while starting the Server:`);
console.error(`${colors.fg.cyan}${err.stack}`);
process.exit(1);
});
}))
.command('generate <folder>', 'Generate a template Project', ((cmd) => cmd
.positional('folder', {
type: 'string',
description: 'The Folder to generate the template in',
demandOption: true
})
.option('template', {
type: 'string',
description: 'Which template to use',
alias: ['E'],
default: 'choose',
})
.option('variant', {
type: 'string',
description: 'The Variant of the template to use',
alias: ['V'],
default: 'choose',
})), (async (args) => {
const { default: inquirer } = await eval('import("inquirer")');
console.log(`${prefix} ${colors.fg.gray}Fetching Templates from GitHub...`);
const templates = [];
JSON.parse((await new Promise((resolve, reject) => {
const chunks = [];
https.get({
path: '/repos/0x7d8/NPM_WEB-SERVER/contents/templates',
host: 'api.github.com',
port: 443,
headers: {
"User-Agent": `rjweb-server@cli ${version}`,
"Accept": 'application/vnd.github.v3+json',
}
}, (res) => {
res.on('data', (data) => {
chunks.push(data);
}).once('error', reject)
.once('end', () => {
resolve(Buffer.concat(chunks));
});
});
})).toString()).filter((t) => t.type === 'dir').forEach((template) => {
const variant = template.name.match(/\[.*\] /)[0].replace(/\[|\]/g, '').trim();
const name = template.name.replace(/\[.*\] /, '');
if (templates.some((t) => t.name === name)) {
const index = templates.findIndex((t) => t.name === name);
templates[index].variants.push({
name: variant,
git: template
});
}
else {
templates.push({
name,
variants: [
{
name: variant,
git: template
}
]
});
}
});
let template = '', variant = '';
if (args.template !== 'choose' && templates.some((t) => t.name === args.template)) {
template = args.template;
console.log(`${prefix} ${colors.fg.gray}Using ${colors.fg.cyan}${template}`);
}
else {
if (args.template !== 'choose')
console.log(`${prefix} ${colors.fg.cyan}${args.template} ${colors.fg.red}is not a valid template!`);
await inquirer.prompt([
{
name: 'Template',
type: 'list',
prefix,
choices: templates.map((t) => t.name),
askAnswered: true
}
]).then((answers) => {
template = answers.Template;
});
}
if (args.variant !== 'choose' && templates.some((t) => t.name === args.variant)) {
variant = args.variant;
console.log(`${prefix} ${colors.fg.gray}Using Variant ${colors.fg.cyan}${variant}`);
}
else {
if (args.variant !== 'choose')
console.log(`${prefix} ${colors.fg.cyan}${args.template} ${colors.fg.red}is not a valid template!`);
await inquirer.prompt([
{
name: 'Variant',
type: 'list',
prefix,
choices: templates.find((t) => t.name === template).variants.map((v) => v.name),
askAnswered: true
}
]).then((answers) => {
variant = answers.Variant;
});
}
if (!fs.existsSync(path.join(process.cwd(), args.folder))) {
await fs.promises.mkdir(path.join(process.cwd(), args.folder));
}
console.log(`${prefix} ${colors.fg.gray}Generating Template Project...`);
const handleDirectory = async (directory) => {
const files = JSON.parse((await new Promise((resolve, reject) => {
const chunks = [];
https.get({
path: new URL(directory).pathname,
host: 'api.github.com',
port: 443,
headers: {
"User-Agent": `rjweb-server@cli ${version}`,
"Accept": 'application/vnd.github.v3+json',
}
}, (res) => {
res.on('data', (data) => {
chunks.push(data);
}).once('error', reject)
.once('end', () => {
resolve(Buffer.concat(chunks));
});
});
})).toString());
if (Array.isArray(files)) {
for (const file of files) {
if (file.type === 'dir') {
if (!fs.existsSync(pPath.join(process.cwd(), args.folder, file.path.replace(`templates/[${variant}] ${template}`, '')))) {
await fs.promises.mkdir(pPath.join(process.cwd(), args.folder, file.path.replace(`templates/[${variant}] ${template}`, '')));
}
}
await handleDirectory(file.url);
}
}
else {
const file = files;
if (file.name === 'yarn.lock')
return;
console.log(`${prefix} ${colors.fg.green}Downloaded ${colors.fg.cyan}${path.join(args.folder, file.path.replace(`templates/[${variant}] ${template}`, ''))}`);
await fs.promises.writeFile(path.join(process.cwd(), args.folder, file.path.replace(`templates/[${variant}] ${template}`, '')), Buffer.from(file.content, 'base64').toString());
}
};
await handleDirectory(templates.find((t) => t.name === template).variants.find((v) => v.name === variant).git.url);
console.log('');
console.log(`${prefix} ${colors.fg.green}Template Project Generated!`);
console.log('');
// Test for Package Managers
let availablePackageManagers = [];
try {
execSync('npm --version', {
stdio: 'ignore'
});
availablePackageManagers.push('npm');
}
catch { }
try {
execSync('yarn --version', {
stdio: 'ignore'
});
availablePackageManagers.push('yarn');
}
catch { }
try {
execSync('pnpm --version', {
stdio: 'ignore'
});
availablePackageManagers.push('pnpm');
}
catch { }
try {
execSync('bun --version', {
stdio: 'ignore'
});
availablePackageManagers.push('bun');
}
catch { }
let continueWith = 'npm';
await inquirer.prompt([
{
name: 'Continue with',
type: 'list',
prefix,
choices: availablePackageManagers,
askAnswered: true
}
]).then((answers) => {
continueWith = answers['Continue with'];
});
const spinner = new Spinner();
const runInterval = () => {
process.stdout.write(`\r${prefix} ${colors.fg.yellow}${spinner.get()} ${colors.fg.gray}Installing Dependencies with ${colors.fg.cyan}${continueWith}${colors.fg.gray}...`);
};
const interval = setInterval(runInterval, 175);
runInterval();
process.chdir(path.join(process.cwd(), args.folder));
exec(`${continueWith} install`, () => {
clearInterval(interval);
process.stdout.write('\n');
console.log(`${prefix} ${colors.fg.green}Installed Dependencies!${colors.reset}`);
});
}))
.help()
.argv;