diff --git a/scripts/build.mjs b/scripts/build.mjs index e27b872..0704972 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -1,528 +1,46 @@ import commonjs from "@rollup/plugin-commonjs"; import nodeResolve from "@rollup/plugin-node-resolve"; import { createHash } from "crypto"; -import { readFile, readdir, writeFile, stat } from "fs/promises"; +import { readFile, readdir, writeFile } from "fs/promises"; import { argv } from "process"; import { rollup, watch } from "rollup"; import swc from "@swc/core"; import esbuild from "rollup-plugin-esbuild"; -// ═══════════════════════════════════════════════════════════════════════════════ -// 🎨 STYLING & ANIMATIONS -// ═══════════════════════════════════════════════════════════════════════════════ - -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 = { - // 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, - ), - ), - ); - - // 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}`); - }); - }, - - 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}`, - ], - ]; - - 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(); +console.log(`Building ${toBuild.length} plugin(s)...`); -// 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; +for (const plugin of toBuild) { 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) : "", - ); - - buildResults.push({ - name: plugin, - status: "success", - time: buildTime, - size: result.size || 0, - }); - - built++; - } + await buildPlugin(plugin); } catch (e) { - 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, - }); - + console.error(e.stack || e); failed++; } } -// Final progress -if (!isWatch) { - console.log( - `\n ${createProgressBar(toBuild.length, toBuild.length)} ${colors.dim}(${toBuild.length}/${toBuild.length})${colors.reset}`, - ); -} +console.log( + "\n" + + (failed + ? "\x1b[31m" + `Failed to build ${failed} plugin(s)` + "\x1b[0m" + : "\x1b[32m" + "All plugin(s) built successfully!" + "\x1b[0m"), +); -// 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 -// ═══════════════════════════════════════════════════════════════════════════════ +isWatch && console.log("\nWatching for changes..."); async function buildPlugin(plugin) { - if (plugin.endsWith(".ts")) return {}; + if (plugin.endsWith(".ts")) return; const manifest = Object.assign( JSON.parse(await readFile("./base_manifest.json")), @@ -554,16 +72,11 @@ async function buildPlugin(plugin) { inlineDynamicImports: true, }, onwarn: (warning) => { - if ( - ![ - "UNRESOLVED_IMPORT", - "MISSING_NAME_OPTION_FOR_IIFE_EXPORT", - "CIRCULAR_DEPENDENCY", - ].includes(warning.code) && - isVerbose - ) { - logger.warning(`${plugin}: ${warning.message}`); - } + ![ + "UNRESOLVED_IMPORT", + "MISSING_NAME_OPTION_FOR_IIFE_EXPORT", + "CIRCULAR_DEPENDENCY", + ].includes(warning.code) && console.warn(warning); }, plugins: [ nodeResolve({ @@ -593,13 +106,13 @@ async function buildPlugin(plugin) { ], }; - const applyHash = async () => { - const content = await readFile(outPath); - return Object.assign(manifest, { - hash: createHash("sha256").update(content).digest("hex"), + const applyHash = async () => + Object.assign(manifest, { + hash: createHash("sha256") + .update(await readFile(outPath)) + .digest("hex"), main: entry, }); - }; if (!isWatch) { const bundle = await rollup(options); @@ -609,9 +122,8 @@ async function buildPlugin(plugin) { await applyHash(); await writeFile(`./dist/${plugin}/manifest.json`, JSON.stringify(manifest)); - // Get file size - const stats = await stat(outPath); - return { size: stats.size }; + console.log(`${plugin}: ` + "\x1b[32m" + "Build succeed!" + "\x1b[0m"); + return; } const watcher = watch(options); @@ -619,10 +131,6 @@ async function buildPlugin(plugin) { 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(); @@ -632,17 +140,18 @@ async function buildPlugin(plugin) { JSON.stringify(manifest), ); - const buildTime = formatTime(event.duration || 0); - logger.plugin.watching(plugin, buildTime); - resolve({ size: 0 }); + console.log( + `${plugin}: ` + + "\x1b[32m" + + `Build succeed! (${event.duration}ms)` + + "\x1b[0m", + ); + resolve(); break; } case "ERROR": case "FATAL": - logger.plugin.error( - plugin, - isVerbose ? event.error.stack : event.error.message, - ); + console.error(`${plugin}: ` + "\x1b[31m", "Failed! :(", "\x1b[0m"); reject(event.error); break; } diff --git a/scripts/serve.mjs b/scripts/serve.mjs index 185c25f..b6f2d03 100755 --- a/scripts/serve.mjs +++ b/scripts/serve.mjs @@ -1,557 +1,18 @@ #!/usr/bin/env node - -import { exec, spawn } from "child_process"; -import { networkInterfaces } from "os"; +import { spawn } from "child_process"; import path from "path"; import { fileURLToPath } from "url"; -import { readdir, stat, access } from "fs/promises"; -import { createHash } from "crypto"; -// ═══════════════════════════════════════════════════════════════════════════════ -// 🎨 STYLING & ANIMATIONS -// ═══════════════════════════════════════════════════════════════════════════════ +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const distPath = path.resolve(__dirname, "..", "dist"); +const port = process.env.PORT || 1400; -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); +const server = spawn("npx", ["serve", "-s", distPath, "-l", port.toString()], { + stdio: "inherit", + shell: process.platform === "win32", +}); + +process.on("SIGINT", () => { + server.kill(); + process.exit(); });