diff --git a/scripts/build.mjs b/scripts/build.mjs index 57fffef..e27b872 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -1,109 +1,651 @@ import commonjs from "@rollup/plugin-commonjs"; import nodeResolve from "@rollup/plugin-node-resolve"; import { createHash } from "crypto"; -import { readFile, readdir, writeFile } from "fs/promises"; +import { readFile, readdir, writeFile, stat } from "fs/promises"; import { argv } from "process"; import { rollup, watch } from "rollup"; + import swc from "@swc/core"; import esbuild from "rollup-plugin-esbuild"; -const args = argv.slice(2); -const isWatch = args.includes("--watch") || args.includes("-w"); -const pluginsToBuild = args.filter((x) => !x.startsWith("-")).length - ? args.filter((x) => !x.startsWith("-")) - : await readdir("./plugins"); +// ═══════════════════════════════════════════════════════════════════════════════ +// 🎨 STYLING & ANIMATIONS +// ═══════════════════════════════════════════════════════════════════════════════ -const successfulPlugins = []; +const colors = { + reset: "\x1b[0m", + bold: "\x1b[1m", + dim: "\x1b[2m", + italic: "\x1b[3m", + underline: "\x1b[4m", -async function buildPlugin(plugin) { - if (plugin.endsWith(".ts")) return; + // Standard colors + black: "\x1b[30m", + red: "\x1b[31m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + magenta: "\x1b[35m", + cyan: "\x1b[36m", + white: "\x1b[37m", - try { - const manifest = Object.assign( - JSON.parse(await readFile("./base_manifest.json")), - JSON.parse(await readFile(`./plugins/${plugin}/manifest.json`)), + // Bright colors + brightRed: "\x1b[91m", + brightGreen: "\x1b[92m", + brightYellow: "\x1b[93m", + brightBlue: "\x1b[94m", + brightMagenta: "\x1b[95m", + brightCyan: "\x1b[96m", + brightWhite: "\x1b[97m", + + // Backgrounds + bgRed: "\x1b[41m", + bgGreen: "\x1b[42m", + bgYellow: "\x1b[43m", + bgBlue: "\x1b[44m", + bgMagenta: "\x1b[45m", + bgCyan: "\x1b[46m", +}; + +const symbols = { + // Build status + success: "✓", + error: "✗", + warning: "⚠", + info: "ℹ", + + // Activities + building: "🔨", + watching: "👁", + rocket: "🚀", + gear: "⚙", + lightning: "⚡", + fire: "🔥", + sparkles: "✨", + + // UI elements + arrow: "→", + bullet: "•", + star: "★", + diamond: "◆", + + // Box drawing + boxLight: "─", + boxHeavy: "━", + boxDouble: "═", + cornerTL: "┌", + cornerTR: "┐", + cornerBL: "└", + cornerBR: "┘", + cross: "┼", + teeLeft: "├", + teeRight: "┤", + vertical: "│", +}; + +const spinners = { + dots: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"], + line: ["│", "╱", "─", "╲"], + bounce: ["⠁", "⠂", "⠄", "⠂"], + pulse: ["●", "◐", "◑", "◒", "◓", "◔", "◕", "◖", "◗"], +}; + +// ═══════════════════════════════════════════════════════════════════════════════ +// 🛠 UTILITIES +// ═══════════════════════════════════════════════════════════════════════════════ + +function formatTime(ms) { + if (ms < 1000) return `${ms}ms`; + if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; + return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`; +} + +function formatSize(bytes) { + if (bytes === 0) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; +} + +function createGradient(text, startColor, endColor) { + // Simple gradient effect by alternating colors + return text + .split("") + .map((char, i) => { + const color = i % 2 === 0 ? startColor : endColor; + return `${color}${char}${colors.reset}`; + }) + .join(""); +} + +function createBox(content, title = "", style = "light") { + const lines = content.split("\n"); + const maxLength = Math.max( + ...lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "").length), + title.length, + ); + const width = maxLength + 4; + + const horizontal = + style === "heavy" + ? symbols.boxHeavy + : style === "double" + ? symbols.boxDouble + : symbols.boxLight; + + let result = ""; + + // Top border + result += `${symbols.cornerTL}${horizontal.repeat(width - 2)}${symbols.cornerTR}\n`; + + // Title + if (title) { + const padding = Math.floor((width - title.length - 4) / 2); + result += `${symbols.vertical} ${" ".repeat(padding)}${colors.bold}${title}${colors.reset}${" ".repeat(padding)} ${symbols.vertical}\n`; + result += `${symbols.teeLeft}${horizontal.repeat(width - 2)}${symbols.teeRight}\n`; + } + + // Content + lines.forEach((line) => { + const cleanLine = line.replace(/\x1b\[[0-9;]*m/g, ""); + const padding = width - cleanLine.length - 4; + result += `${symbols.vertical} ${line}${" ".repeat(padding)} ${symbols.vertical}\n`; + }); + + // Bottom border + result += `${symbols.cornerBL}${horizontal.repeat(width - 2)}${symbols.cornerBR}`; + + return result; +} + +class Spinner { + constructor(text = "", spinnerType = "dots") { + this.text = text; + this.frames = spinners[spinnerType]; + this.current = 0; + this.isSpinning = false; + this.interval = null; + } + + start() { + if (this.isSpinning) return; + this.isSpinning = true; + + this.interval = setInterval(() => { + process.stdout.write( + `\r${colors.cyan}${this.frames[this.current]}${colors.reset} ${this.text}`, + ); + this.current = (this.current + 1) % this.frames.length; + }, 100); + } + + succeed(text) { + this.stop(); + console.log( + `${colors.green}${symbols.success}${colors.reset} ${text || this.text}`, + ); + } + + fail(text) { + this.stop(); + console.log( + `${colors.red}${symbols.error}${colors.reset} ${text || this.text}`, + ); + } + + stop() { + if (!this.isSpinning) return; + this.isSpinning = false; + clearInterval(this.interval); + process.stdout.write("\r\x1b[K"); // Clear line + } +} + +function createProgressBar(current, total, width = 40, showPercentage = true) { + const percentage = Math.round((current / total) * 100); + const filled = Math.round((current / total) * width); + const empty = width - filled; + + const filledBar = `${colors.brightCyan}${"█".repeat(filled)}${colors.reset}`; + const emptyBar = `${colors.dim}${"░".repeat(empty)}${colors.reset}`; + + let result = `${filledBar}${emptyBar}`; + + if (showPercentage) { + result += ` ${colors.bold}${percentage.toString().padStart(3)}%${colors.reset}`; + } + + return result; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// 🎭 LOGGER +// ═══════════════════════════════════════════════════════════════════════════════ + +const logger = { + banner() { + const title = createGradient( + "VENDETTA PLUGIN BUILDER", + colors.brightCyan, + colors.brightMagenta, + ); + const subtitle = `${colors.dim}Professional-grade TypeScript plugin compilation${colors.reset}`; + + console.log(); + console.log(` ${symbols.rocket} ${title}`); + console.log(` ${subtitle}`); + console.log(); + }, + + section(title) { + const line = symbols.boxHeavy.repeat(60); + console.log(`\n${colors.brightBlue}${line}${colors.reset}`); + console.log(`${colors.bold}${colors.brightBlue} ${title}${colors.reset}`); + console.log(`${colors.brightBlue}${line}${colors.reset}\n`); + }, + + info(message, icon = symbols.info) { + console.log(`${colors.brightBlue}${icon}${colors.reset} ${message}`); + }, + + success(message, icon = symbols.success) { + console.log(`${colors.brightGreen}${icon}${colors.reset} ${message}`); + }, + + warning(message, icon = symbols.warning) { + console.log(`${colors.brightYellow}${icon}${colors.reset} ${message}`); + }, + + error(message, icon = symbols.error) { + console.log(`${colors.brightRed}${icon}${colors.reset} ${message}`); + }, + + plugin: { + building(name) { + return new Spinner( + `${colors.bold}${name}${colors.reset} ${colors.dim}Building...${colors.reset}`, + "pulse", + ); + }, + + success(name, time, size) { + const timeStr = `${colors.dim}${time}${colors.reset}`; + const sizeStr = size ? `${colors.dim}${size}${colors.reset}` : ""; + console.log( + `${colors.brightGreen}${symbols.success}${colors.reset} ${colors.bold}${name}${colors.reset} ${timeStr} ${sizeStr}`, + ); + }, + + error(name, error) { + console.log( + `${colors.brightRed}${symbols.error}${colors.reset} ${colors.bold}${name}${colors.reset}`, + ); + if (error) { + console.log(`${colors.red}${colors.dim} ${error}${colors.reset}`); + } + }, + + watching(name, time) { + const timeStr = `${colors.dim}${time}${colors.reset}`; + console.log( + `${colors.brightCyan}${symbols.lightning}${colors.reset} ${colors.bold}${name}${colors.reset} ${colors.dim}rebuilt${colors.reset} ${timeStr}`, + ); + }, + }, + + table(headers, rows) { + const colWidths = headers.map((header, i) => + Math.max( + header.length, + ...rows.map( + (row) => + (row[i] || "").toString().replace(/\x1b\[[0-9;]*m/g, "").length, + ), + ), ); - const entry = "index.js"; - const outPath = `./dist/${plugin}/${entry}`; + // Header + const headerRow = headers + .map( + (header, i) => + `${colors.bold}${header.padEnd(colWidths[i])}${colors.reset}`, + ) + .join(` ${colors.dim}│${colors.reset} `); - const options = { - input: `./plugins/${plugin}/${manifest.main}`, - external: (id) => - id.startsWith("@vendetta") || ["react", "react-native"].includes(id), - output: { - file: outPath, - format: "iife", - compact: true, - exports: "named", - name: "plugin", - globals: (id) => { - if (id.startsWith("@vendetta")) - return id.substring(1).replace(/\//g, "."); - if (id === "react") return "window.React"; - if (id === "react-native") return "vendetta.metro.common.ReactNative"; - return null; - }, - }, - onwarn: () => {}, - plugins: [ - nodeResolve(), - commonjs(), - { - name: "swc", - transform: (code, id) => - swc.transform(code, { - filename: id, - jsc: { - parser: { syntax: "typescript", tsx: true }, - externalHelpers: true, - }, - env: { targets: "defaults" }, - }), - }, - esbuild({ minify: true }), + console.log(` ${headerRow}`); + + // Separator + const separator = colWidths + .map((width) => symbols.boxLight.repeat(width)) + .join(`${colors.dim}─┼─${colors.reset}`); + console.log(` ${colors.dim}${separator}${colors.reset}`); + + // Rows + rows.forEach((row) => { + const formattedRow = row + .map((cell, i) => { + const cleanCell = (cell || "") + .toString() + .replace(/\x1b\[[0-9;]*m/g, ""); + const padding = colWidths[i] - cleanCell.length; + return cell + " ".repeat(padding); + }) + .join(` ${colors.dim}│${colors.reset} `); + + console.log(` ${formattedRow}`); + }); + }, + + summary(stats) { + const { built, failed, total, totalTime, watchMode } = stats; + + const rows = [ + ["Total Plugins", total.toString()], + ["Built Successfully", `${colors.brightGreen}${built}${colors.reset}`], + [ + "Failed", + failed > 0 + ? `${colors.brightRed}${failed}${colors.reset}` + : `${colors.dim}0${colors.reset}`, ], - }; + ["Total Time", formatTime(totalTime)], + [ + "Mode", + watchMode + ? `${colors.brightCyan}Watch${colors.reset}` + : `${colors.dim}Build${colors.reset}`, + ], + ]; - const finalize = async () => { - const content = await readFile(outPath); - manifest.hash = createHash("sha256").update(content).digest("hex"); - manifest.main = entry; - await writeFile( - `./dist/${plugin}/manifest.json`, - JSON.stringify(manifest), + this.table(["Metric", "Value"], rows); + }, +}; + +// ═══════════════════════════════════════════════════════════════════════════════ +// 🚀 MAIN EXECUTION +// ═══════════════════════════════════════════════════════════════════════════════ + +const args = argv.slice(2); +console.clear(); + +// Parse arguments +const inputPlugins = args.filter((x) => !x.startsWith("-")); +const flags = args.map((x) => x.toLowerCase()).filter((x) => x.startsWith("-")); + +const isWatch = flags.includes("--watch") || flags.includes("-w"); +const isVerbose = flags.includes("--verbose") || flags.includes("-v"); +const toBuild = inputPlugins.length ? inputPlugins : await readdir("./plugins"); + +// Display banner +logger.banner(); + +// Build configuration +const config = { + plugins: toBuild.length, + mode: isWatch ? "Watch" : "Build", + verbose: isVerbose, + startTime: Date.now(), +}; + +logger.info( + `${colors.dim}Found ${colors.reset}${colors.bold}${config.plugins}${colors.reset}${colors.dim} plugin(s) to ${config.mode.toLowerCase()}${colors.reset}`, +); +if (isWatch) { + logger.info( + `${colors.dim}Running in watch mode - changes will trigger rebuilds${colors.reset}`, + ); +} + +// Build statistics +let built = 0; +let failed = 0; +const buildResults = []; +const startTime = Date.now(); + +// Build all plugins +logger.section("Building Plugins"); + +for (let i = 0; i < toBuild.length; i++) { + const plugin = toBuild[i]; + + if (!isWatch) { + console.log( + `\n ${createProgressBar(i, toBuild.length)} ${colors.dim}(${i}/${toBuild.length})${colors.reset}`, + ); + } + + const pluginStartTime = Date.now(); + let spinner; + + try { + if (!plugin.endsWith(".ts")) { + spinner = logger.plugin.building(plugin); + spinner.start(); + + const result = await buildPlugin(plugin); + const buildTime = Date.now() - pluginStartTime; + + spinner.stop(); + logger.plugin.success( + plugin, + formatTime(buildTime), + result.size ? formatSize(result.size) : "", ); - }; - if (isWatch) { - const watcher = watch(options); - watcher.on("event", async (event) => { - if (event.code === "BUNDLE_END") { - await finalize(); - event.result.close(); - console.log(`[WATCH] Rebuilt ${plugin}`); - } else if (event.code === "ERROR") { - console.error(`[ERROR] ${plugin}: ${event.error.message}`); - } + buildResults.push({ + name: plugin, + status: "success", + time: buildTime, + size: result.size || 0, }); - } else { - const bundle = await rollup(options); - await bundle.write(options.output); - await bundle.close(); - await finalize(); - successfulPlugins.push(plugin); + + built++; } } catch (e) { - console.error(`[ERROR] ${plugin}: ${e.message}`); + if (spinner) spinner.stop(); + + logger.plugin.error(plugin, isVerbose ? e.stack : e.message); + + buildResults.push({ + name: plugin, + status: "error", + time: Date.now() - pluginStartTime, + error: e.message, + }); + + failed++; } } -// Start processing -const buildPromises = pluginsToBuild.map((p) => buildPlugin(p)); - +// Final progress if (!isWatch) { - await Promise.all(buildPromises); - if (successfulPlugins.length > 0) { - console.log(`Successfully built: ${successfulPlugins.join(", ")}`); - } + console.log( + `\n ${createProgressBar(toBuild.length, toBuild.length)} ${colors.dim}(${toBuild.length}/${toBuild.length})${colors.reset}`, + ); +} + +// Summary +logger.section("Build Summary"); + +const totalTime = Date.now() - startTime; +logger.summary({ + built, + failed, + total: toBuild.length, + totalTime, + watchMode: isWatch, +}); + +// Results table +if (buildResults.length > 0 && !isWatch) { + console.log(); + const tableRows = buildResults.map((result) => [ + result.name, + result.status === "success" + ? `${colors.brightGreen}${symbols.success} Success${colors.reset}` + : `${colors.brightRed}${symbols.error} Failed${colors.reset}`, + formatTime(result.time), + result.size ? formatSize(result.size) : `${colors.dim}N/A${colors.reset}`, + ]); + + logger.table(["Plugin", "Status", "Time", "Size"], tableRows); +} + +// Final status +console.log(); +if (failed > 0) { + logger.error( + `Build completed with ${failed} error(s) ${symbols.bullet} ${built} succeeded`, + symbols.warning, + ); +} else { + logger.success( + `All plugins built successfully! ${symbols.sparkles}`, + symbols.fire, + ); +} + +// Watch mode notification +if (isWatch) { + console.log(); + logger.info( + `Watching for changes... ${colors.dim}Press ${colors.reset}${colors.bold}Ctrl+C${colors.reset}${colors.dim} to exit${colors.reset}`, + symbols.watching, + ); +} + +console.log(); + +// ═══════════════════════════════════════════════════════════════════════════════ +// 🔧 BUILD FUNCTION +// ═══════════════════════════════════════════════════════════════════════════════ + +async function buildPlugin(plugin) { + if (plugin.endsWith(".ts")) return {}; + + const manifest = Object.assign( + JSON.parse(await readFile("./base_manifest.json")), + JSON.parse(await readFile(`./plugins/${plugin}/manifest.json`)), + ); + + const entry = "index.js"; + const outPath = `./dist/${plugin}/${entry}`; + + /** @type {import("rollup").RollupOptions} */ + const options = { + input: `./plugins/${plugin}/${manifest.main}`, + output: { + file: outPath, + globals(id) { + if (id.startsWith("@vendetta")) + return id.substring(1).replace(/\//g, "."); + + const map = { + react: "window.React", + "react-native": "vendetta.metro.common.ReactNative", + }; + + return map[id] || null; + }, + format: "iife", + compact: true, + exports: "named", + inlineDynamicImports: true, + }, + onwarn: (warning) => { + if ( + ![ + "UNRESOLVED_IMPORT", + "MISSING_NAME_OPTION_FOR_IIFE_EXPORT", + "CIRCULAR_DEPENDENCY", + ].includes(warning.code) && + isVerbose + ) { + logger.warning(`${plugin}: ${warning.message}`); + } + }, + plugins: [ + nodeResolve({ + resolveOnly: (id) => !["react", "react-native"].includes(id), + }), + commonjs(), + { + name: "swc", + transform(code, id) { + return swc.transform(code, { + filename: id, + jsc: { + parser: { + syntax: "typescript", + tsx: true, + }, + externalHelpers: true, + }, + env: { + targets: "defaults", + include: ["transform-classes", "transform-arrow-functions"], + }, + }); + }, + }, + esbuild({ minify: true }), + ], + }; + + const applyHash = async () => { + const content = await readFile(outPath); + return Object.assign(manifest, { + hash: createHash("sha256").update(content).digest("hex"), + main: entry, + }); + }; + + if (!isWatch) { + const bundle = await rollup(options); + await bundle.write(options.output); + await bundle.close(); + + await applyHash(); + await writeFile(`./dist/${plugin}/manifest.json`, JSON.stringify(manifest)); + + // Get file size + const stats = await stat(outPath); + return { size: stats.size }; + } + + const watcher = watch(options); + + return await new Promise((resolve, reject) => { + watcher.on("event", async (event) => { + switch (event.code) { + case "BUNDLE_START": + // Silent in watch mode, spinner handles this + break; + + case "BUNDLE_END": { + event.result.close(); + + await applyHash(); + await writeFile( + `./dist/${plugin}/manifest.json`, + JSON.stringify(manifest), + ); + + const buildTime = formatTime(event.duration || 0); + logger.plugin.watching(plugin, buildTime); + resolve({ size: 0 }); + break; + } + case "ERROR": + case "FATAL": + logger.plugin.error( + plugin, + isVerbose ? event.error.stack : event.error.message, + ); + reject(event.error); + break; + } + }); + }); } diff --git a/scripts/serve.mjs b/scripts/serve.mjs index b6f2d03..185c25f 100755 --- a/scripts/serve.mjs +++ b/scripts/serve.mjs @@ -1,18 +1,557 @@ #!/usr/bin/env node -import { spawn } from "child_process"; + +import { exec, spawn } from "child_process"; +import { networkInterfaces } from "os"; import path from "path"; import { fileURLToPath } from "url"; +import { readdir, stat, access } from "fs/promises"; +import { createHash } from "crypto"; -const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const distPath = path.resolve(__dirname, "..", "dist"); -const port = process.env.PORT || 1400; +// ═══════════════════════════════════════════════════════════════════════════════ +// 🎨 STYLING & ANIMATIONS +// ═══════════════════════════════════════════════════════════════════════════════ -const server = spawn("npx", ["serve", "-s", distPath, "-l", port.toString()], { - stdio: "inherit", - shell: process.platform === "win32", -}); - -process.on("SIGINT", () => { - server.kill(); - process.exit(); +const colors = { + reset: "\x1b[0m", + bold: "\x1b[1m", + dim: "\x1b[2m", + italic: "\x1b[3m", + underline: "\x1b[4m", + + // Standard colors + black: "\x1b[30m", + red: "\x1b[31m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + magenta: "\x1b[35m", + cyan: "\x1b[36m", + white: "\x1b[37m", + + // Bright colors + brightRed: "\x1b[91m", + brightGreen: "\x1b[92m", + brightYellow: "\x1b[93m", + brightBlue: "\x1b[94m", + brightMagenta: "\x1b[95m", + brightCyan: "\x1b[96m", + brightWhite: "\x1b[97m", + + // Backgrounds + bgRed: "\x1b[41m", + bgGreen: "\x1b[42m", + bgYellow: "\x1b[43m", + bgBlue: "\x1b[44m", + bgMagenta: "\x1b[45m", + bgCyan: "\x1b[46m", +}; + +const symbols = { + // Status indicators + success: "✓", + error: "✗", + warning: "⚠", + info: "ℹ", + + // Activities + server: "🚀", + folder: "📁", + network: "🌐", + local: "🏠", + external: "📡", + + lightning: "⚡", + fire: "🔥", + sparkles: "✨", + eyes: "👀", + stop: "🛑", + gear: "⚙", + globe: "🌍", + + // UI elements + arrow: "→", + bullet: "•", + star: "★", + diamond: "◆", + + // Box drawing + boxLight: "─", + boxHeavy: "━", + boxDouble: "═", + cornerTL: "┌", + cornerTR: "┐", + cornerBL: "└", + cornerBR: "┘", + cross: "┼", + teeLeft: "├", + teeRight: "┤", + vertical: "│", +}; + +// ═══════════════════════════════════════════════════════════════════════════════ +// 🛠 UTILITIES +// ═══════════════════════════════════════════════════════════════════════════════ + +function createGradient(text, startColor, endColor) { + return text + .split("") + .map((char, i) => { + const color = i % 2 === 0 ? startColor : endColor; + return `${color}${char}${colors.reset}`; + }) + .join(""); +} + +function createBox(content, title = "", style = "light") { + const lines = content.split("\n"); + const maxLength = Math.max( + ...lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "").length), + title.length, + ); + const width = maxLength + 4; + + const horizontal = + style === "heavy" + ? symbols.boxHeavy + : style === "double" + ? symbols.boxDouble + : symbols.boxLight; + + let result = ""; + + // Top border + result += `${symbols.cornerTL}${horizontal.repeat(width - 2)}${symbols.cornerTR}\n`; + + // Title + if (title) { + const padding = Math.floor((width - title.length - 4) / 2); + result += `${symbols.vertical} ${" ".repeat(padding)}${colors.bold}${title}${colors.reset}${" ".repeat(padding)} ${symbols.vertical}\n`; + result += `${symbols.teeLeft}${horizontal.repeat(width - 2)}${symbols.teeRight}\n`; + } + + // Content + lines.forEach((line) => { + const cleanLine = line.replace(/\x1b\[[0-9;]*m/g, ""); + const padding = width - cleanLine.length - 4; + result += `${symbols.vertical} ${line}${" ".repeat(padding)} ${symbols.vertical}\n`; + }); + + // Bottom border + result += `${symbols.cornerBL}${horizontal.repeat(width - 2)}${symbols.cornerBR}`; + + return result; +} + +function formatFileSize(bytes) { + if (bytes === 0) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// 🌐 NETWORK UTILITIES +// ═══════════════════════════════════════════════════════════════════════════════ + +function getAllNetworkInterfaces() { + const nets = networkInterfaces(); + const results = []; + + for (const name of Object.keys(nets)) { + for (const net of nets[name]) { + if (net.family === "IPv4" && !net.internal) { + results.push({ + interface: name, + address: net.address, + netmask: net.netmask, + mac: net.mac, + }); + } + } + } + + return results; +} + +function getPrimaryIP() { + const interfaces = getAllNetworkInterfaces(); + + // Prefer WiFi/Ethernet interfaces + const preferred = interfaces.find( + (iface) => + iface.interface.toLowerCase().includes("wlan") || + iface.interface.toLowerCase().includes("wifi") || + iface.interface.toLowerCase().includes("eth") || + iface.interface.toLowerCase().includes("en"), + ); + + return preferred ? preferred.address : interfaces[0]?.address || "localhost"; +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// 📊 FILE SYSTEM UTILITIES +// ═══════════════════════════════════════════════════════════════════════════════ + +async function analyzeDistDirectory(distPath) { + try { + await access(distPath); + const entries = await readdir(distPath); + + const plugins = []; + let totalSize = 0; + + for (const entry of entries) { + try { + const entryPath = path.join(distPath, entry); + const entryStats = await stat(entryPath); + + if (entryStats.isDirectory()) { + // Analyze plugin directory + const pluginFiles = await readdir(entryPath); + let pluginSize = 0; + + for (const file of pluginFiles) { + const filePath = path.join(entryPath, file); + const fileStats = await stat(filePath); + if (fileStats.isFile()) { + pluginSize += fileStats.size; + } + } + + plugins.push({ + name: entry, + size: pluginSize, + files: pluginFiles.length, + }); + + totalSize += pluginSize; + } + } catch (error) { + // Skip entries that can't be read + } + } + + return { + plugins, + totalSize, + totalPlugins: plugins.length, + }; + } catch (error) { + return { + plugins: [], + totalSize: 0, + totalPlugins: 0, + error: error.message, + }; + } +} + +// ═══════════════════════════════════════════════════════════════════════════════ +// 🎭 LOGGER +// ═══════════════════════════════════════════════════════════════════════════════ + +const logger = { + banner() { + const title = createGradient( + "VENDETTA DEV SERVER", + colors.brightCyan, + colors.brightMagenta, + ); + const subtitle = `${colors.dim}Professional development server for Vendetta plugins${colors.reset}`; + + console.log(); + console.log(` ${symbols.server} ${title}`); + console.log(` ${subtitle}`); + console.log(); + }, + + section(title) { + const line = symbols.boxHeavy.repeat(60); + console.log(`\n${colors.brightBlue}${line}${colors.reset}`); + console.log(`${colors.bold}${colors.brightBlue} ${title}${colors.reset}`); + console.log(`${colors.brightBlue}${line}${colors.reset}\n`); + }, + + info(message, icon = symbols.info) { + console.log(`${colors.brightBlue}${icon}${colors.reset} ${message}`); + }, + + success(message, icon = symbols.success) { + console.log(`${colors.brightGreen}${icon}${colors.reset} ${message}`); + }, + + warning(message, icon = symbols.warning) { + console.log(`${colors.brightYellow}${icon}${colors.reset} ${message}`); + }, + + error(message, icon = symbols.error) { + console.log(`${colors.brightRed}${icon}${colors.reset} ${message}`); + }, + + table(headers, rows) { + const colWidths = headers.map((header, i) => + Math.max( + header.length, + ...rows.map( + (row) => + (row[i] || "").toString().replace(/\x1b\[[0-9;]*m/g, "").length, + ), + ), + ); + + // Header + const headerRow = headers + .map( + (header, i) => + `${colors.bold}${header.padEnd(colWidths[i])}${colors.reset}`, + ) + .join(` ${colors.dim}│${colors.reset} `); + + console.log(` ${headerRow}`); + + // Separator + const separator = colWidths + .map((width) => symbols.boxLight.repeat(width)) + .join(`${colors.dim}─┼─${colors.reset}`); + console.log(` ${colors.dim}${separator}${colors.reset}`); + + // Rows + rows.forEach((row) => { + const formattedRow = row + .map((cell, i) => { + const cleanCell = (cell || "") + .toString() + .replace(/\x1b\[[0-9;]*m/g, ""); + const padding = colWidths[i] - cleanCell.length; + return cell + " ".repeat(padding); + }) + .join(` ${colors.dim}│${colors.reset} `); + + console.log(` ${formattedRow}`); + }); + }, +}; + +// ═══════════════════════════════════════════════════════════════════════════════ +// 🚀 MAIN EXECUTION +// ═══════════════════════════════════════════════════════════════════════════════ + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + +async function main() { + console.clear(); + + // Configuration + const distPath = path.join(__dirname, "..", "dist"); + const port = process.env.PORT || 1400; + const host = "0.0.0.0"; + const primaryIP = getPrimaryIP(); + const allInterfaces = getAllNetworkInterfaces(); + + // Display banner + logger.banner(); + + // Analyze distribution directory + logger.section("Analyzing Distribution"); + + const analysis = await analyzeDistDirectory(distPath); + + if (analysis.error) { + logger.error(`Cannot access dist directory: ${analysis.error}`); + logger.info( + `Make sure to run ${colors.bold}npm run build${colors.reset} first`, + ); + process.exit(1); + } + + logger.info( + `${colors.dim}Serving from${colors.reset} ${colors.bold}${distPath}${colors.reset}`, + ); + logger.success( + `Found ${colors.bold}${analysis.totalPlugins}${colors.reset} plugin(s) ${colors.dim}(${formatFileSize(analysis.totalSize)} total)${colors.reset}`, + ); + + // Plugin table + if (analysis.plugins.length > 0) { + console.log(); + const pluginRows = analysis.plugins.map((plugin) => [ + plugin.name, + plugin.files.toString(), + formatFileSize(plugin.size), + ]); + + logger.table(["Plugin", "Files", "Size"], pluginRows); + } + + // Network information + logger.section("Network Configuration"); + + const localUrl = `http://localhost:${port}`; + const networkUrl = `http://${primaryIP}:${port}`; + + logger.info( + `${colors.dim}Binding to${colors.reset} ${colors.bold}${host}:${port}${colors.reset}`, + ); + + // Network interfaces table + if (allInterfaces.length > 0) { + console.log(); + const networkRows = allInterfaces.map((iface) => [ + iface.interface, + iface.address, + `http://${iface.address}:${port}`, + ]); + + logger.table(["Interface", "IP Address", "URL"], networkRows); + } + + // Access URLs + logger.section("Access Information"); + + logger.success( + `${symbols.local} Local: ${colors.bold}${colors.underline}${localUrl}${colors.reset}`, + ); + logger.success( + `${symbols.network} Network: ${colors.bold}${colors.underline}${networkUrl}${colors.reset}`, + ); + logger.info( + `${symbols.globe} External: ${colors.dim}Accessible from any device on your network${colors.reset}`, + ); + + // Server startup + logger.section("Starting Server"); + + logger.info(`${symbols.lightning} Initializing development server...`); + + // Check if serve is available + try { + await new Promise((resolve, reject) => { + exec("npx serve --version", (error) => { + if (error) reject(error); + else resolve(); + }); + }); + } catch (error) { + logger.warning("Installing serve package..."); + } + + const serverProcess = spawn( + "npx", + ["serve", "-s", distPath, "-l", port.toString()], + { + stdio: ["pipe", "pipe", "pipe"], + shell: process.platform === "win32", + }, + ); + + // Handle server output + let serverStarted = false; + + serverProcess.stdout.on("data", (data) => { + const output = data.toString(); + + if ( + !serverStarted && + (output.includes("Accepting connections") || output.includes("Local:")) + ) { + serverStarted = true; + + logger.success(`${symbols.fire} Server is running!`); + console.log(); + + // Instructions + logger.info( + `${symbols.eyes} ${colors.dim}Watching for file changes...${colors.reset}`, + ); + logger.info( + `${symbols.stop} ${colors.dim}Press ${colors.reset}${colors.bold}Ctrl+C${colors.reset}${colors.dim} to stop the server${colors.reset}`, + ); + + console.log(); + console.log( + `${colors.brightGreen}${symbols.sparkles} Ready to develop! ${symbols.sparkles}${colors.reset}`, + ); + console.log(); + } + + // Filter out serve's own output to avoid duplication + if ( + !output.includes("Local:") && + !output.includes("On Your Network:") && + !output.includes("Copied local address") && + !output.includes("Accepting connections") && + output.trim() + ) { + console.log(`${colors.dim}[serve] ${output.trim()}${colors.reset}`); + } + }); + + serverProcess.stderr.on("data", (data) => { + const error = data.toString().trim(); + if (error && !error.includes("WARNING") && !error.includes("deprecated")) { + logger.error(`Server error: ${error}`); + } + }); + + serverProcess.on("error", (error) => { + logger.error(`Failed to start server: ${error.message}`); + process.exit(1); + }); + + serverProcess.on("exit", (code) => { + if (code !== 0 && code !== null) { + logger.error(`Server exited with code ${code}`); + process.exit(code); + } + }); + + // Graceful shutdown + const shutdown = (signal) => { + console.log( + `\n${colors.brightYellow}${symbols.stop} Received ${signal}, shutting down gracefully...${colors.reset}`, + ); + + if (serverProcess && !serverProcess.killed) { + serverProcess.kill("SIGTERM"); + + setTimeout(() => { + if (!serverProcess.killed) { + logger.warning("Force killing server process..."); + serverProcess.kill("SIGKILL"); + } + }, 5000); + } + + setTimeout(() => { + logger.success("Server stopped successfully"); + process.exit(0); + }, 1000); + }; + + process.on("SIGINT", () => shutdown("SIGINT")); + process.on("SIGTERM", () => shutdown("SIGTERM")); + + // Handle uncaught exceptions + process.on("uncaughtException", (error) => { + logger.error(`Uncaught exception: ${error.message}`); + console.error(error.stack); + shutdown("UNCAUGHT_EXCEPTION"); + }); + + process.on("unhandledRejection", (reason, promise) => { + logger.error(`Unhandled rejection at: ${promise}, reason: ${reason}`); + shutdown("UNHANDLED_REJECTION"); + }); +} + +// Start the server +main().catch((error) => { + console.error( + `${colors.brightRed}${symbols.error} Fatal error:${colors.reset}`, + error.message, + ); + console.error(error.stack); + process.exit(1); });