Merge pull request #193 from SKaranjaN/feature/plugin-json-improvements

feat: improve plugin.json with multiple authors, versions, and pre-compilation dependency checking
This commit is contained in:
Jasper
2026-02-11 20:40:20 +01:00
committed by GitHub
4 changed files with 129 additions and 5 deletions
+46
View File
@@ -28,6 +28,7 @@ const fs = require("fs")
const ts = require("typescript")
const {createHash,Hash} = require("crypto")
const nodepath = require('path')
const ansis = require("ansis")
/** ## What is this?
* This is a function which compares `./src/` with a hash stored in `./dist/hash.txt`.
@@ -84,6 +85,51 @@ function saveNewCompilationHash(){
}
if (!process.argv.includes("--no-compile")){
const requiredDependencies = new Set()
if (fs.existsSync("./plugins")){
console.log("OT: Reading plugin.json files...")
for (const pluginDir of fs.readdirSync("./plugins")){
if (pluginDir === ".DS_Store") continue
const pluginPath = nodepath.join("./plugins", pluginDir)
if (!fs.statSync(pluginPath).isDirectory()) continue
const pluginJsonPath = nodepath.join(pluginPath, "plugin.json")
if (fs.existsSync(pluginJsonPath)){
try{
const pluginData = JSON.parse(fs.readFileSync(pluginJsonPath).toString())
if (pluginData.npmDependencies && Array.isArray(pluginData.npmDependencies)){
pluginData.npmDependencies.forEach((dep) => {
if (typeof dep === "string" && dep.trim()){
requiredDependencies.add(dep.trim())
}
})
}
}catch(err){
// skip invalid plugin.json files, will be caught later
}
}
}
if (requiredDependencies.size > 0){
console.log("OT: Checking plugin npm dependencies...")
/**@type {string[]} */
const missingDeps = []
for (const dep of requiredDependencies){
try{
require.resolve(dep)
}catch(err){
missingDeps.push(dep)
}
}
if (missingDeps.length > 0){
console.log(ansis.red("OT: ❌ Fatal Error --> Missing npm dependencies required by plugins:\n\n")+ansis.cyan(missingDeps.map((dep) => " - "+dep).join("\n")+"\n"))
console.log("OT: Please install missing dependencies using the following command:\n> "+ansis.bold.green("npm install " + missingDeps.join(" "))+"\n")
process.exit(1)
}
}
}
if (requiresCompilation()){
console.log("OT: Compilation Required...")
+2
View File
@@ -3,6 +3,7 @@
"id":"example-plugin",
"version":"1.0.0",
"startFile":"index.ts",
"supportedVersions":["OTv4.0.x","OTv4.1.x"],
"enabled":false,
"priority":0,
@@ -14,6 +15,7 @@
"details":{
"author":"DJj123dj",
"contributors":[],
"shortDescription":"A simple template for an Open Ticket v4 plugin!",
"longDescription":"A simple example of an Open Ticket v4 plugin!",
"imageUrl":"",
+13 -2
View File
@@ -55,6 +55,11 @@ export interface ODPluginData {
version:string,
/**The location of the start file of the plugin relative to the rootDir of the plugin */
startFile:string,
/**A list of compatible versions. (e.g. `["OTv4.0.x", "OMv1.x.x"]`) (optional, will be required in future version)
* - `OT` --> Open Ticket support
* - `OM` --> Open Moderation support
*/
supportedVersions?:string[],
/**Is this plugin enabled? */
enabled:boolean,
@@ -78,8 +83,10 @@ export interface ODPluginData {
* Additional details in the `plugin.json` file from a plugin.
*/
export interface ODPluginDetails {
/**The author of the plugin. */
/**The main author of the plugin. Additional contributors can be specified in `contributors`. */
author:string,
/**A list of plugin contributors. (optional, will be required in future version) */
contributors?:string[],
/**A short description of this plugin. */
shortDescription:string,
/**A large description of this plugin. */
@@ -121,7 +128,7 @@ export class ODPlugin extends ODManagerData {
/**Did this plugin crash? (A reason is available in the `crashReason`) */
crashed: boolean
/**The reason which caused this plugin to crash. */
crashReason: null|"incompatible.plugin"|"missing.plugin"|"missing.dependency"|"executed" = null
crashReason: null|"incompatible.plugin"|"missing.plugin"|"missing.dependency"|"incompatible.version"|"executed" = null
constructor(dir:string, jsondata:ODPluginData){
super(jsondata.id)
@@ -212,6 +219,10 @@ export class ODPlugin extends ODManagerData {
return incompatible
}
/**Get a list of all authors & contributors of this plugin. */
getAuthors(): string[] {
return [this.details.author,...(this.details.contributors ?? [])]
}
}
/**## ODPluginClassManager `class`
+68 -3
View File
@@ -12,6 +12,7 @@ export const loadAllPlugins = async () => {
return
}
const plugins = fs.readdirSync("./plugins")
const pluginVersionRegex = /^(OT|OM)v(\d+)\.(\d+|x)\.(\d+|x)$/
//check & validate
plugins.forEach((p) => {
@@ -38,6 +39,20 @@ export const loadAllPlugins = async () => {
if (typeof rawplugindata.version != "string") throw new ODPluginError("Failed to load plugin.json/version")
if (typeof rawplugindata.startFile != "string") throw new ODPluginError("Failed to load plugin.json/startFile")
//only check "supportedVersions" if it exists (should be array)
if (rawplugindata.supportedVersions){
if (!Array.isArray(rawplugindata.supportedVersions)) throw new ODPluginError("Failed to load plugin.json/supportedVersions (must be array)")
for (const version of rawplugindata.supportedVersions){
if (typeof version !== "string"){
throw new ODPluginError("Failed to load plugin.json/supportedVersions (all items must be strings)")
}
//only OT (Open Ticket) & OM (Open Moderation) are supported at the moment
if (!pluginVersionRegex.test(version)){
throw new ODPluginError(`Failed to load plugin.json/supportedVersions (invalid format: "${version}", expected format like "OTv4.0.x" or "OMv1.0.0")`)
}
}
}
if (typeof rawplugindata.enabled != "boolean") throw new ODPluginError("Failed to load plugin.json/enabled")
if (typeof rawplugindata.priority != "number") throw new ODPluginError("Failed to load plugin.json/priority")
if (!Array.isArray(rawplugindata.events)) throw new ODPluginError("Failed to load plugin.json/events")
@@ -47,7 +62,11 @@ export const loadAllPlugins = async () => {
if (!Array.isArray(rawplugindata.incompatiblePlugins)) throw new ODPluginError("Failed to load plugin.json/incompatiblePlugins")
if (typeof rawplugindata.details != "object") throw new ODPluginError("Failed to load plugin.json/details")
if (typeof rawplugindata.details.author != "string") throw new ODPluginError("Failed to load plugin.json/details/author")
if (typeof rawplugindata.details.author != "string") throw new ODPluginError("Failed to load plugin.json/details/author (must be string or array)")
//only check "contributors" if it exists (should be array)
if (rawplugindata.details.contributors && !Array.isArray(rawplugindata.details.contributors)) throw new ODPluginError("Failed to load plugin.json/details/contributors (must be array)")
if (typeof rawplugindata.details.shortDescription != "string") throw new ODPluginError("Failed to load plugin.json/details/shortDescription")
if (typeof rawplugindata.details.longDescription != "string") throw new ODPluginError("Failed to load plugin.json/details/longDescription")
if (typeof rawplugindata.details.imageUrl != "string") throw new ODPluginError("Failed to load plugin.json/details/imageUrl")
@@ -91,6 +110,7 @@ export const loadAllPlugins = async () => {
const incompatibilities: {from:string,to:string}[] = []
const missingDependencies: {id:string,missing:string}[] = []
const missingPlugins: {id:string,missing:string}[] = []
const versionIncompatibilities: {id:string}[] = []
//go through all plugins for errors
sortedPlugins.filter((plugin) => plugin.enabled).forEach((plugin) => {
@@ -98,6 +118,33 @@ export const loadAllPlugins = async () => {
plugin.dependenciesInstalled().forEach((missing) => missingDependencies.push({id:from,missing}))
plugin.pluginsIncompatible(opendiscord.plugins).forEach((incompatible) => incompatibilities.push({from,to:incompatible}))
plugin.pluginsInstalled(opendiscord.plugins).forEach((missing) => missingPlugins.push({id:from,missing}))
//check if plugins are compatible with version of bot
if (plugin.data.supportedVersions && plugin.data.supportedVersions.length > 0){
const currentVersion = opendiscord.versions.get("opendiscord:version")
let isCompatible = false
for (const versionStr of plugin.data.supportedVersions){
const match = versionStr.match(pluginVersionRegex)
if (!match) continue
const projectPrefix = match[1]
const primary = parseInt(match[2])
const secondary = (match[3] === "x") ? null : parseInt(match[3])
const tertiary = (match[4] === "x") ? null : parseInt(match[4])
if (projectPrefix !== "OT") continue
else if (primary !== currentVersion.primary) continue
else if (typeof secondary === "number" && secondary !== currentVersion.secondary) continue
else if (typeof tertiary === "number" && tertiary !== currentVersion.tertiary) continue
else{
isCompatible = true
break
}
}
if (!isCompatible) versionIncompatibilities.push({id:from})
}
})
//handle all incompatibilities
@@ -152,6 +199,22 @@ export const loadAllPlugins = async () => {
initPluginError = true
})
//handle all bot version incompatibilities
versionIncompatibilities.forEach((match) => {
const plugin = opendiscord.plugins.get(match.id)
if (plugin && !plugin.crashed){
plugin.crashed = true
plugin.crashReason = "incompatible.version"
}
const versions = plugin?.data.supportedVersions?.join(", ") ?? "<unknown-version>"
const currentVersion = opendiscord.versions.get("opendiscord:version").toString()
opendiscord.log(`Plugin version incompatibility: plugin requires "${versions}" but current bot version is "${currentVersion}", canceling plugin execution...`,"plugin",[
{key:"path",value:"./plugins/"+match.id}
])
initPluginError = true
})
//exit on error (when soft mode disabled)
if (!opendiscord.defaults.getDefault("softPluginLoading") && initPluginError){
console.log("")
@@ -177,17 +240,19 @@ export const loadAllPlugins = async () => {
}
for (const plugin of sortedPlugins){
const authors = [plugin.details.author,...(plugin.details.contributors ?? [])].join(", ")
if (plugin.enabled){
opendiscord.debug.debug("Plugin \""+plugin.id.value+"\" loaded",[
{key:"status",value:(plugin.crashed ? "crashed" : "success")},
{key:"crashReason",value:(plugin.crashed ? (plugin.crashReason ?? "/") : "/")},
{key:"author",value:plugin.details.author},
{key:"authors",value:authors},
{key:"version",value:plugin.version.toString()},
{key:"priority",value:plugin.priority.toString()}
])
}else{
opendiscord.debug.debug("Plugin \""+plugin.id.value+"\" disabled",[
{key:"author",value:plugin.details.author},
{key:"authors",value:authors},
{key:"version",value:plugin.version.toString()},
{key:"priority",value:plugin.priority.toString()}
])