651 lines
18 KiB
JavaScript
651 lines
18 KiB
JavaScript
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 { 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();
|
||
|
||
// 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) : "",
|
||
);
|
||
|
||
buildResults.push({
|
||
name: plugin,
|
||
status: "success",
|
||
time: buildTime,
|
||
size: result.size || 0,
|
||
});
|
||
|
||
built++;
|
||
}
|
||
} 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,
|
||
});
|
||
|
||
failed++;
|
||
}
|
||
}
|
||
|
||
// Final progress
|
||
if (!isWatch) {
|
||
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;
|
||
}
|
||
});
|
||
});
|
||
}
|