Skip to content
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ electron-builder.env
node_modules/
bundle.js
workbox-*.js
service_worker.js
service_worker.js
test-env/
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// FIXME - This file should be removed before the PR is merged!
{
"files.exclude": {
"**/.git": true,
"**/node_modules": true
},
"editor.formatOnSave": false,
}
8 changes: 8 additions & 0 deletions js/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,14 @@ const Blockbench = {
if (LastVersion && compareVersions(version, LastVersion) && !Blockbench.isOlderThan(version)) {
callback(LastVersion);
}
},
/**
* Logs output to the terminal Blockbench was started from with a fancy Blockbench prefix
*
* If the first argument is 'NO_PREFIX', the output won't include the "[Blockbench]" prefix
*/
log(...args) {
app.terminal.log(...args)
}
};

Expand Down
5 changes: 5 additions & 0 deletions js/boot_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ localStorage.setItem('last_version', Blockbench.version);
loadInfoFromURL();
}
proceeded = true;
Blockbench.dispatchEvent('all_plugins_loaded')
}
loadInstalledPlugins().then(proceed);
setTimeout(proceed, 1200);
Expand All @@ -165,4 +166,8 @@ setStartScreen(true);

document.getElementById('page_wrapper').classList.remove('invisible');

if (process.env.BLOCKBENCH_OPEN_DEV_TOOLS === 'TRUE') {
electron.getCurrentWindow().openDevTools()
}

Blockbench.setup_successful = true;
147 changes: 147 additions & 0 deletions js/cli.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
const { app, ipcMain } = require('electron')
const PACKAGE = require('../package.json')
const { Command } = require('commander')
const pathjs = require('path')

const program = new Command()

program.name('blockbench').description(PACKAGE.description)

program
.option('-v, --version', 'output the version number', () => {
app.terminal.log('NO_PREFIX', PACKAGE.version)
app.exit(0)
})
.option('--userData <path>', 'change the folder Blockbench uses to store user data')
.option('--no-auto-update', 'disables auto update')
.option('--install-custom-plugins <paths...>', 'install plugins from the given paths or URLS on startup')
.option('--install-plugins <ids...>', 'install plugins by ID from the Blockbench plugin repository on startup')
.option('--clean-installed-plugins', 'remove all installed plugins on startup')
.option('--open-dev-tools', 'open the developer tools on startup')
.option('--hidden', 'hide the main window')
// Custom Error Handling
.exitOverride(error => {
switch (error.code) {
case 'commander.help':
case 'commander.helpDisplayed':
case 'commander.version':
app.exit(0)
case 'commander.unknownOption':
case 'commander.excessArguments':
// Uses ANSI escape codes to clear the previous line and print a warning message.
app.terminal.log('NO_PREFIX', '\x1b[1A\x1b[2K\x1b[33mUse --help to see available options\x1b[0m')
app.exit(1)
default:
app.terminal.error('\x1b[2;31m%s\x1b[0m', error)
app.exit(1)
}
})
.configureOutput({
getOutHasColors: () => true,
writeErr: str => {
console.error('\x1b[91m%s\x1b[0m', str)
},
})
// Custom Help Styling
.configureHelp({
styleTitle(str) {
return `\x1b[1m${str}\x1b[0m`
},
styleCommandText(str) {
return `\x1b[36m${str}\x1b[0m`
},
styleCommandDescription(str) {
return `\x1b[1A\x1b[2K\x1b[35m${str}\x1b[0m`
},
styleDescriptionText(str) {
return `\x1b[3m${str}\x1b[0m`
},
styleOptionText(str) {
return `\x1b[32m${str}\x1b[0m`
},
styleArgumentText(str) {
return `\x1b[33m${str}\x1b[0m`
},
styleSubcommandText(str) {
return `\x1b[34m${str}\x1b[0m`
},
})

/**
* Makes sure the environment variables are set to defaults if they are not set.
*
* Allows for overriding the default values by setting the environment variables before starting Blockbench.
*/
function affirmEnvironmentVariables() {
process.env.BLOCKBENCH_AUTO_UPDATE ??= 'ENABLED'
process.env.BLOCKBENCH_CLEAN_INSTALLED_PLUGINS ??= 'FALSE'
process.env.BLOCKBENCH_INSTALL_CUSTOM_PLUGINS ??= ''
process.env.BLOCKBENCH_INSTALL_PLUGINS ??= ''
process.env.BLOCKBENCH_OPEN_DEV_TOOLS ??= 'FALSE'
process.env.BLOCKBENCH_USER_DATA ??= app.getPath('userData')
process.env.BLOCKBENCH_HIDDEN ??= 'FALSE'
}

function parseCLI() {
// Parse command line arguments.
program.parse()
/**
* @type {{ userData?: string, autoUpdate?: boolean, withPluginFiles?: string[], withPluginUrls?: string[] }}
*/
let {
userData,
autoUpdate,
installCustomPlugins,
installPlugins,
openDevTools,
cleanInstalledPlugins,
hidden,
} = program.opts()

// --no-auto-update
if (autoUpdate === false) {
process.env.BLOCKBENCH_AUTO_UPDATE = 'DISABLED'
}

// --userData
if (userData) {
if (!pathjs.isAbsolute(userData)) {
// Automatically resolve relative paths.
userData = pathjs.resolve(userData)
}
process.env.BLOCKBENCH_USER_DATA = userData
}
app.setPath('userData', process.env.BLOCKBENCH_USER_DATA)

// --clean-installed-plugins
if (cleanInstalledPlugins) {
process.env.BLOCKBENCH_CLEAN_INSTALLED_PLUGINS = 'TRUE'
}
// --install-custom-plugins
if (installCustomPlugins?.length > 0) {
process.env.BLOCKBENCH_INSTALL_CUSTOM_PLUGINS = installCustomPlugins.join(',')
}
// --install-plugins
if (installPlugins?.length > 0) {
process.env.BLOCKBENCH_INSTALL_PLUGINS = installPlugins.join(',')
}
// --open-dev-tools
if (openDevTools) {
process.env.BLOCKBENCH_OPEN_DEV_TOOLS = 'TRUE'
}
// --hidden
if (hidden) {
process.env.BLOCKBENCH_HIDDEN = 'TRUE'
}
}

module.exports = function cli() {
affirmEnvironmentVariables()

try {
parseCLI()
} catch (error) {
app.terminal.error(error)
app.exit(1)
}
}
147 changes: 144 additions & 3 deletions js/plugin_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,32 @@ const Plugins = {
});
}
}

const ENVIRONMENT_CUSTOM_PLUGINS = []
const ENVIRONMENT_PLUGINS = []
if (process.env.BLOCKBENCH_INSTALL_CUSTOM_PLUGINS) {
ENVIRONMENT_CUSTOM_PLUGINS.push(...process.env.BLOCKBENCH_INSTALL_CUSTOM_PLUGINS
.split(',')
.map(file => file.trim())
)
}
if (process.env.BLOCKBENCH_INSTALL_PLUGINS) {
ENVIRONMENT_PLUGINS.push(...process.env.BLOCKBENCH_INSTALL_PLUGINS
.split(',')
.map(url => url.trim())
)
}

StateMemory.init('installed_plugins', 'array')
Plugins.installed = StateMemory.installed_plugins = StateMemory.installed_plugins.filter(p => p && typeof p == 'object');

if (process.env.BLOCKBENCH_CLEAN_INSTALLED_PLUGINS === 'TRUE') {
Plugins.installed = StateMemory.installed_plugins = StateMemory.installed_plugins.filter(
p => p && typeof p == 'object' && !ENVIRONMENT_CUSTOM_PLUGINS.includes(p.path) && !ENVIRONMENT_PLUGINS.includes(p.id)
)
} else {
app.terminal.log('--clean-installed-plugins: Clearing installed plugins')
Plugins.installed = StateMemory.installed_plugins = []
}

async function runPluginFile(path, plugin_id) {
let file_content;
Expand Down Expand Up @@ -663,6 +687,16 @@ class Plugin {
}
return this.details;
}

/**
* Logs output to the terminal Blockbench was started from with a fancy plugin-specific prefix
* @example
* myPlugin.log('Hello World!')
* /// [Blockbench] <my-plugin> Hello World!
*/
log(...args) {
Blockbench.log(`\x1b[90m<\x1b[33m${this.id}\x1b[90m>\x1b[0m`, ...args)
}
}
Plugin.prototype.menu = new Menu([
new MenuSeparator('installation'),
Expand Down Expand Up @@ -867,7 +901,6 @@ async function loadInstalledPlugins() {
if (Plugins.installed.length > 0) {
var load_counter = 0;
Plugins.installed.forEachReverse(function(plugin) {

if (plugin.source == 'file') {
//Dev Plugins
if (isApp && fs.existsSync(plugin.path)) {
Expand Down Expand Up @@ -901,7 +934,115 @@ async function loadInstalledPlugins() {
console.log(`Loaded ${load_counter} plugin${pluralS(load_counter)}`)
}
StateMemory.save('installed_plugins')


// CLI Environment Plugins
for (const path of ENVIRONMENT_CUSTOM_PLUGINS) {
const id = PathModule.basename(path, '.js');
const pathType = path.startsWith('http') ? 'URL' : 'File';

const alreadyInstalled = Plugins.installed.find(plugin => plugin.id === id)
if (alreadyInstalled) {
app.terminal.error(`Failed to install Environment plugin "${id}":`)
app.terminal.error(`A Plugin with the ID "${id}" already exists in the installed plugins list!`)
app.exit(1)
}

// Remove the plugin from the installed plugins list when Blockbench is closed.
Blockbench.on('before_closing', () => {
const plugin = Plugins.installed.find(plugin => plugin.id === id)
Plugins.installed.remove(plugin)
StateMemory.save('installed_plugins')
app.terminal.log(`Uninstalled Environment plugin "${id}"`)
})

if (pathType === 'URL') {
app.terminal.log(`Installing Environment plugin "${id || url}" from URL...`);
if (!(Plugins.json instanceof Object && navigator.onLine)) {
app.terminal.error(`Failed to install Environment plugins:`)
app.terminal.error(`Blockbench cannot install plugins by URL when offline.`)
app.exit(1)
}
const instance = new Plugin(id);
install_promises.push(instance.loadFromURL(url, false)
.then(() => {
app.terminal.log(`Loaded Environment plugin "${id || url}" from URL`);
console.log(`🧩🏠🌐 Loaded Environment plugin "${id || url}" from URL`);
})
.catch(err => {
app.terminal.error(`Failed to load Environment plugin "${id || url}":`)
app.terminal.error(err)
app.exit(1)
})
);
} else {
if (!fs.existsSync(path)) {
app.terminal.error(`Failed to install Environment plugin "${id}":`)
app.terminal.error(`The specified plugin file does not exist: "${path}"`)
app.exit(1)
}
app.terminal.log(`Installing Environment plugin "${id || path}" from file...`);
const instance = new Plugin(id);
install_promises.push(instance.loadFromFile({path}, false)
.then(() => {
app.terminal.log(`Loaded Environment plugin "${id || path}" from file`);
console.log(`🧩🏠📁 Loaded Environment plugin "${id || path}" from file`);
})
.catch(err => {
app.terminal.error(`Failed to load Environment plugin "${id || path}":`)
app.terminal.error(err)
app.exit(1)
})
);
}
}

// Cannot install plugins by URL when offline
if (ENVIRONMENT_PLUGINS.length > 0 && !(Plugins.json instanceof Object && navigator.onLine)) {
app.terminal.error(`Failed to install Environment plugins:`)
app.terminal.error(`Blockbench cannot install plugins by URL when offline.`)
app.exit(1)
}

for (const id of ENVIRONMENT_PLUGINS) {
const exists = Plugins.json[id] instanceof Object
if (!exists) {
app.terminal.error(`Failed to install Environment plugin "${id}":`)
app.terminal.error(`Plugin data for "${id}" does not exist in the plugin repository.`)
app.exit(1)
}
const alreadyInstalled = Plugins.installed.find(plugin => plugin.id === id)
if (alreadyInstalled) {
app.terminal.error(`Failed to install Environment plugin "${id}":`)
app.terminal.error(`A Plugin with the ID "${id}" already exists in the installed plugins list!`)
app.exit(1)
}

// Remove the plugin from the installed plugins list when Blockbench is closed.
Blockbench.on('before_closing', () => {
const plugin = Plugins.installed.find(plugin => plugin.id === id)
Plugins.installed.remove(plugin)
StateMemory.save('installed_plugins')
app.terminal.log(`Uninstalled Environment plugin "${id}"`)
})

app.terminal.log(`Installing Environment plugin "${id}"`);
const plugin = new Plugin(id, Plugins.json[id])
install_promises.push(
plugin.download().then(() => {
if (!Plugins.json[id]) {
app.terminal.error(`Failed to install Environment plugin "${id}":`)
app.terminal.error(`Failed to fetch plugin data.`)
app.exit(1)
}
app.terminal.log(`Installed Environment plugin "${id}"`);
console.log(`🧩🏠📁 Installed Environment plugin "${id}"`);
}).catch(err => {
app.terminal.error(`Failed to install Environment plugin "${id}":`)
app.terminal.error(err)
app.exit(1)
})
)
}

install_promises.forEach(promise => {
promise.catch(console.error);
Expand Down
Loading