feat: improve plugin.json with multiple authors, versions, and pre-compilation dependency checking

- Add support for multiple authors and contributors in plugin.json
- Maintain backwards compatibility with single author (string) format
- Add versions array to specify compatible Open Ticket versions (e.g., OTv4.0.x, ODv1.0.0)
- Implement version compatibility checking during plugin loading
- Read plugin.json files before TypeScript compilation to check npm dependencies
- Warn about missing npm dependencies before compilation starts
- Update example plugin to demonstrate new features

Fixes #162
This commit is contained in:
Karanja-Stephen
2026-02-10 13:13:24 +03:00
parent 90851f881d
commit 779efca39a
4 changed files with 189 additions and 5 deletions
+48
View File
@@ -84,6 +84,54 @@ function saveNewCompilationHash(){
}
if (!process.argv.includes("--no-compile")){
// Read plugin.json files before compilation to check for npm dependencies
const pluginDependencies = new Set()
if (fs.existsSync("./plugins")){
console.log("OT: Reading plugin.json files...")
const plugins = fs.readdirSync("./plugins")
for (const pluginDir of 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()) {
pluginDependencies.add(dep.trim())
}
})
}
} catch (e) {
// Silently skip invalid plugin.json files - they'll be caught during plugin loading
}
}
}
// Check for missing dependencies
if (pluginDependencies.size > 0){
console.log("OT: Checking plugin npm dependencies...")
const missingDeps = []
for (const dep of pluginDependencies){
try {
require.resolve(dep)
} catch {
missingDeps.push(dep)
}
}
if (missingDeps.length > 0){
console.log("OT: Warning - Missing npm dependencies required by plugins:")
missingDeps.forEach(dep => console.log(` - ${dep}`))
console.log("OT: Please install missing dependencies with: npm install " + missingDeps.join(" "))
console.log("OT: Continuing compilation anyway...")
}
}
}
if (requiresCompilation()){
console.log("OT: Compilation Required...")
+3
View File
@@ -14,6 +14,9 @@
"details":{
"author":"DJj123dj",
"authors":["DJj123dj"],
"contributors":[],
"versions":["OTv4.0.x","OTv4.1.x"],
"shortDescription":"A simple template for an Open Ticket v4 plugin!",
"longDescription":"A simple example of an Open Ticket v4 plugin!",
"imageUrl":"",
+30 -2
View File
@@ -78,8 +78,14 @@ export interface ODPluginData {
* Additional details in the `plugin.json` file from a plugin.
*/
export interface ODPluginDetails {
/**The author of the plugin. */
author:string,
/**The author of the plugin. (string for backwards compatibility, or string[] for multiple authors) */
author:string|string[],
/**A list of authors of the plugin. (new format, optional if author is provided) */
authors?:string[],
/**A list of contributors to the plugin. (optional) */
contributors?:string[],
/**A list of compatible versions. (e.g. ["OTv4.0.x", "OTv4.1.x", "ODv1.0.0"]) */
versions?:string[],
/**A short description of this plugin. */
shortDescription:string,
/**A large description of this plugin. */
@@ -212,6 +218,28 @@ export class ODPlugin extends ODManagerData {
return incompatible
}
/**Get all authors as an array. Handles both old (string) and new (array) format. */
getAuthors(): string[] {
if (Array.isArray(this.details.author)) {
return this.details.author
} else if (this.details.authors && Array.isArray(this.details.authors)) {
return this.details.authors
} else if (typeof this.details.author === "string") {
return [this.details.author]
}
return []
}
/**Get all contributors as an array. */
getContributors(): string[] {
return this.details.contributors || []
}
/**Get all compatible versions as an array. */
getCompatibleVersions(): string[] {
return this.details.versions || []
}
}
/**## ODPluginClassManager `class`
+108 -3
View File
@@ -47,7 +47,49 @@ 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")
// Handle author field - support both old (string) and new (array) format for backwards compatibility
if (typeof rawplugindata.details.author != "string" && !Array.isArray(rawplugindata.details.author)) {
throw new ODPluginError("Failed to load plugin.json/details/author (must be string or array)")
}
// Normalize author to array format for internal use
if (typeof rawplugindata.details.author == "string") {
// Old format: convert string to array
rawplugindata.details.authors = [rawplugindata.details.author]
} else if (Array.isArray(rawplugindata.details.author)) {
// New format: use author array as authors
rawplugindata.details.authors = rawplugindata.details.author
}
// Validate authors array if provided separately
if (rawplugindata.details.authors && !Array.isArray(rawplugindata.details.authors)) {
throw new ODPluginError("Failed to load plugin.json/details/authors (must be array)")
}
// Validate contributors array if provided
if (rawplugindata.details.contributors && !Array.isArray(rawplugindata.details.contributors)) {
throw new ODPluginError("Failed to load plugin.json/details/contributors (must be array)")
}
// Validate versions array if provided
if (rawplugindata.details.versions) {
if (!Array.isArray(rawplugindata.details.versions)) {
throw new ODPluginError("Failed to load plugin.json/details/versions (must be array)")
}
// Validate version format: should match pattern like "OTv4.0.x", "ODv1.0.0", etc.
for (const version of rawplugindata.details.versions) {
if (typeof version != "string") {
throw new ODPluginError("Failed to load plugin.json/details/versions (all items must be strings)")
}
// Check format: project prefix (OT, OD, OM, OU) + v + version pattern
const versionPattern = /^(OT|OD|OM|OU)v\d+\.\d+(\.\d+|\.x)$/
if (!versionPattern.test(version)) {
throw new ODPluginError(`Failed to load plugin.json/details/versions (invalid format: "${version}", expected format like "OTv4.0.x" or "ODv1.0.0")`)
}
}
}
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 +133,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 +141,48 @@ 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 version compatibility
if (plugin.data.details.versions && plugin.data.details.versions.length > 0) {
const currentVersion = opendiscord.versions.get("opendiscord:version")
let isCompatible = false
for (const versionStr of plugin.data.details.versions) {
// Parse version string (e.g., "OTv4.0.x" or "OTv4.1.2")
const match = versionStr.match(/^(OT|OD|OM|OU)v(\d+)\.(\d+)(?:\.(\d+|x))$/)
if (!match) continue
const projectPrefix = match[1]
const primary = parseInt(match[2])
const secondary = parseInt(match[3])
const tertiary = match[4]
// Only check OT (Open Ticket) versions for now
if (projectPrefix !== "OT") continue
// Check if version matches
if (tertiary === "x") {
// Wildcard version (e.g., "OTv4.0.x" matches 4.0.0, 4.0.1, etc.)
if (currentVersion.primary === primary && currentVersion.secondary === secondary) {
isCompatible = true
break
}
} else {
// Exact version (e.g., "OTv4.0.0")
const requiredVersion = api.ODVersion.fromString("temp", `v${primary}.${secondary}.${parseInt(tertiary)}`)
if (currentVersion.primary === requiredVersion.primary &&
currentVersion.secondary === requiredVersion.secondary &&
currentVersion.tertiary === requiredVersion.tertiary) {
isCompatible = true
break
}
}
}
if (!isCompatible) {
versionIncompatibilities.push({id:from})
}
}
})
//handle all incompatibilities
@@ -152,6 +237,22 @@ export const loadAllPlugins = async () => {
initPluginError = true
})
//handle all version incompatibilities
versionIncompatibilities.forEach((match) => {
const plugin = opendiscord.plugins.get(match.id)
if (plugin && !plugin.crashed){
plugin.crashed = true
plugin.crashReason = "missing.dependency" // Reuse this reason for version incompatibility
}
const versions = plugin?.data.details.versions?.join(", ") ?? "unknown"
const currentVersion = opendiscord.versions.get("opendiscord:version").toString()
opendiscord.log(`Plugin version incompatibility: plugin requires "${versions}" but current 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 +278,21 @@ export const loadAllPlugins = async () => {
}
for (const plugin of sortedPlugins){
// Get authors list (normalized to array)
const authors = (Array.isArray(plugin.details.author) ? plugin.details.author :
(plugin.details.authors || [plugin.details.author as string])).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:"author",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:"author",value:authors},
{key:"version",value:plugin.version.toString()},
{key:"priority",value:plugin.priority.toString()}
])