2023-02-11 23:09:54 +08:00
import commonjs from "@rollup/plugin-commonjs" ;
import nodeResolve from "@rollup/plugin-node-resolve" ;
2023-02-13 00:48:48 +08:00
import { createHash } from "crypto" ;
2025-12-27 15:57:32 +01:00
import { readFile , readdir , writeFile , stat } from "fs/promises" ;
2023-02-13 00:18:39 +08:00
import { argv } from "process" ;
2023-02-13 22:22:21 +08:00
import { rollup , watch } from "rollup" ;
2025-12-27 15:57:32 +01:00
2023-03-08 15:43:18 +08:00
import swc from "@swc/core" ;
import esbuild from "rollup-plugin-esbuild" ;
2023-01-24 15:56:43 +00:00
2025-12-27 15:57:32 +01:00
// ═══════════════════════════════════════════════════════════════════════════════
// 🎨 STYLING & ANIMATIONS
// ═══════════════════════════════════════════════════════════════════════════════
2025-10-04 11:16:42 +02:00
2025-12-27 15:57:32 +01:00
const colors = {
reset : "\x1b[0m" ,
bold : "\x1b[1m" ,
dim : "\x1b[2m" ,
italic : "\x1b[3m" ,
underline : "\x1b[4m" ,
2025-10-04 11:16:42 +02:00
2025-12-27 15:57:32 +01:00
// 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" ,
2025-10-04 11:16:42 +02:00
2025-12-27 15:57:32 +01:00
// 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 } ` ,
2025-12-27 15:53:27 +01:00
) ;
2025-12-27 15:57:32 +01:00
}
2025-10-04 11:16:42 +02:00
2025-12-27 15:57:32 +01:00
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 } ` ,
2025-12-27 15:53:27 +01:00
] ,
2025-12-27 15:57:32 +01:00
[ "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 ) : "" ,
2025-10-04 11:16:42 +02:00
) ;
2023-02-13 20:42:39 +08:00
2025-12-27 15:57:32 +01:00
buildResults . push ( {
name : plugin ,
status : "success" ,
time : buildTime ,
size : result . size || 0 ,
2025-10-04 11:16:42 +02:00
} ) ;
2025-12-27 15:57:32 +01:00
built ++ ;
2023-02-13 22:22:21 +08:00
}
2025-10-04 11:16:42 +02:00
} catch ( e ) {
2025-12-27 15:57:32 +01:00
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 ++ ;
2025-10-04 11:16:42 +02:00
}
}
2025-12-27 15:57:32 +01:00
// Final progress
2025-12-27 15:53:27 +01:00
if ( ! isWatch ) {
2025-12-27 15:57:32 +01:00
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 } ;
2025-10-04 11:16:42 +02:00
}
2025-12-27 15:57:32 +01:00
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 ;
}
} ) ;
} ) ;
2023-02-11 23:09:54 +08:00
}