Zipped Addons & Bugfixes

This commit is contained in:
Strencher 2023-03-22 20:18:07 +01:00
parent b0001f181e
commit f03138f897
7 changed files with 104 additions and 34 deletions

View File

@ -44,6 +44,7 @@ importers:
babel-plugin-module-resolver: ^4.1.0
circular-dependency-plugin: ^5.2.2
css-loader: ^6.5.1
fflate: ^0.7.4
postcss: ^8.4.5
postcss-cli: ^9.1.0
postcss-csso: ^6.0.0
@ -52,6 +53,8 @@ importers:
stylelint: ^14.3.0
stylelint-config-standard: ^24.0.0
webpack: ^5.73.0
dependencies:
fflate: 0.7.4
devDependencies:
'@babel/core': 7.18.6
'@babel/preset-env': 7.18.6_@babel+core@7.18.6
@ -2544,6 +2547,10 @@ packages:
reusify: 1.0.4
dev: true
/fflate/0.7.4:
resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==}
dev: false
/file-entry-cache/6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
engines: {node: ^10.12.0 || >=12.0.0}

View File

@ -30,7 +30,10 @@
"postcss-easy-import": "^4.0.0",
"postcss-loader": "^6.2.1",
"stylelint": "^14.3.0",
"webpack": "^5.73.0",
"stylelint-config-standard": "^24.0.0"
"stylelint-config-standard": "^24.0.0",
"webpack": "^5.73.0"
},
"dependencies": {
"fflate": "^0.7.4"
}
}

View File

@ -8,6 +8,7 @@ import DiscordModules from "./discordmodules";
import Strings from "./strings";
import AddonEditor from "../ui/misc/addoneditor";
import FloatingWindows from "../ui/floatingwindows";
import {unzipSync} from "fflate";
const React = DiscordModules.React;
@ -26,14 +27,17 @@ const stripBOM = function(fileContent) {
return fileContent;
};
const convertToString = TextDecoder.prototype.decode.bind(new TextDecoder());
export default class AddonManager {
get name() {return "";}
get extension() {return "";}
get extensions() {return [];}
get duplicatePattern() {return /./;}
get addonFolder() {return "";}
get language() {return "";}
get prefix() {return "addon";}
get langExtension() {return "";}
emit(event, ...args) {return Events.emit(`${this.prefix}-${event}`, ...args);}
constructor() {
@ -172,25 +176,75 @@ export default class AddonManager {
return out;
}
assignMeta({meta, filename, fileContent, zip = false} = {}) {
const stats = fs.statSync(filename);
if (!meta.author) meta.author = Strings.Addons.unknownAuthor;
if (!meta.version) meta.version = "???";
if (!meta.description) meta.description = Strings.Addons.noDescription;
meta.id = meta.name || path.basename(filename);
meta.slug = path.basename(filename).replace(this.extensions[zip ? 1 : 0], "").replace(/ /g, "-");
meta.filename = path.basename(filename);
meta.added = stats.atimeMs;
meta.modified = stats.mtimeMs;
meta.size = stats.size;
meta.fileContent = fileContent;
meta.zip = zip;
}
requireZipped(filename) {
const content = fs.readFileSync(filename, "");
const contents = unzipSync(content);
const entryFile = `index${this.langExtension}`;
if (!contents["config.json"]) throw new AddonError(filename, filename, Strings.Addons.metaNotFound, {message: "", stack: ""}, this.prefix);
if (!contents[entryFile]) throw new AddonError(filename, filename, "Zipped addon missing index file.", {message: `Missing ${entryFile} file.`, stack: ""}, this.prefix);
const addon = JSON.parse(convertToString(contents["config.json"]));
this.assignMeta({
meta: addon,
fileContent: convertToString(contents[entryFile]),
zip: true,
filename: filename
});
return addon;
}
// Subclasses should overload this and modify the addon using the fileContent as needed to "require()"" the file
requireAddon(filename) {
requireOld(filename) {
let fileContent = fs.readFileSync(filename, "utf8");
fileContent = stripBOM(fileContent);
const stats = fs.statSync(filename);
const addon = this.extractMeta(fileContent, path.basename(filename));
if (!addon.author) addon.author = Strings.Addons.unknownAuthor;
if (!addon.version) addon.version = "???";
if (!addon.description) addon.description = Strings.Addons.noDescription;
// if (!addon.name || !addon.author || !addon.description || !addon.version) return new AddonError(addon.name || path.basename(filename), filename, "Addon is missing name, author, description, or version", {message: "Addon must provide name, author, description, and version.", stack: ""}, this.prefix);
addon.id = addon.name || path.basename(filename);
addon.slug = path.basename(filename).replace(this.extension, "").replace(/ /g, "-");
addon.filename = path.basename(filename);
addon.added = stats.atimeMs;
addon.modified = stats.mtimeMs;
addon.size = stats.size;
addon.fileContent = fileContent;
if (this.addonList.find(c => c.id == addon.id)) throw new AddonError(addon.name, filename, Strings.Addons.alreadyExists.format({type: this.prefix, name: addon.name}), this.prefix);
this.assignMeta({
meta: addon,
fileContent: fileContent,
filename: filename
});
return addon;
}
requireAddon(filename) {
const ext = path.extname(filename);
const oldExt = path.extname(this.extensions[0]);
const addon = (() => {
switch (ext) {
case ".zip": return this.requireZipped(filename);
case oldExt: return this.requireOld(filename);
default: {
const name = path.basename(filename);
throw new AddonError(name, filename, "Unsupported file format: " + ext, {stack: "", message: ""}, this.prefix);
}
}
})();
if (this.addonList.find(c => c.id == addon.id))
throw new AddonError(addon.name, filename, Strings.Addons.alreadyExists.format({type: this.prefix, name: addon.name}), this.prefix);
this.addonList.push(addon);
return addon;
}
@ -201,14 +255,17 @@ export default class AddonManager {
try {
addon = this.requireAddon(path.resolve(this.addonFolder, filename));
}
catch (e) {
catch (error) {
const partialAddon = this.addonList.find(c => c.filename == filename);
if (partialAddon) {
partialAddon.partial = true;
this.state[partialAddon.id] = false;
this.emit("loaded", partialAddon);
}
return e;
Logger.stacktrace(this.name, `Unable to compile ${path.basename(filename)}.`, error);
return error;
}
@ -220,7 +277,7 @@ export default class AddonManager {
return error;
}
if (shouldToast) Toasts.success(Strings.Addons.wasUnloaded.format({name: addon.name, version: addon.version}));
if (shouldToast) Toasts.success(Strings.Addons.wasLoaded.format({name: addon.name, version: addon.version}));
this.emit("loaded", addon);
if (!this.state[addon.id]) return this.state[addon.id] = false;
@ -309,7 +366,7 @@ export default class AddonManager {
if (!stats || !stats.isFile()) continue;
this.timeCache[filename] = stats.mtime.getTime();
if (!filename.endsWith(this.extension)) {
if (!this.extensions.some(ext => filename.endsWith(ext))) {
// Lets check to see if this filename has the duplicated file pattern `something(1).ext`
const match = filename.match(this.duplicatePattern);
if (!match) continue;
@ -394,4 +451,4 @@ export default class AddonManager {
confirmationText: Strings.Addons.confirmationText.format({name: addon.name})
});
}
}
}

View File

@ -23,11 +23,12 @@ if (typeof(module.exports) !== "function") {
export default new class PluginManager extends AddonManager {
get name() {return "PluginManager";}
get extension() {return ".plugin.js";}
get duplicatePattern() {return /\.plugin\s?\([0-9]+\)\.js/;}
get extensions() {return [".plugin.js", ".plugin.zip"];}
get duplicatePattern() {return /\.plugin\s?\([0-9]+\)\.(js|zip)/;}
get addonFolder() {return path.resolve(Config.dataPath, "plugins");}
get prefix() {return "plugin";}
get language() {return "javascript";}
get langExtension() {return ".js";}
constructor() {
super();
@ -149,7 +150,7 @@ export default new class PluginManager extends AddonManager {
this.state[addon.id] = false;
Toasts.error(Strings.Addons.couldNotStart.format({name: addon.name, version: addon.version}));
Logger.stacktrace(this.name, `${addon.name} v${addon.version} could not be started.`, err);
return new AddonError(addon.name, addon.filename, Strings.Addons.enabled.format({method: "start()"}), {message: err.message, stack: err.stack}, this.prefix);
return new AddonError(addon.name, addon.filename, Strings.Addons.methodError.format({method: "start()"}), {message: err.message, stack: err.stack}, this.prefix);
}
this.emit("started", addon.id);
Toasts.show(Strings.Addons.enabled.format({name: addon.name, version: addon.version}));
@ -165,8 +166,8 @@ export default new class PluginManager extends AddonManager {
catch (err) {
this.state[addon.id] = false;
Toasts.error(Strings.Addons.couldNotStop.format({name: addon.name, version: addon.version}));
Logger.stacktrace(this.name, `${addon.name} v${addon.version} could not be started.`, err);
return new AddonError(addon.name, addon.filename, Strings.Addons.enabled.format({method: "stop()"}), {message: err.message, stack: err.stack}, this.prefix);
Logger.stacktrace(this.name, `${addon.name} v${addon.version} could not be stopped.`, err);
return new AddonError(addon.name, addon.filename, Strings.Addons.methodError.format({method: "stop()"}), {message: err.message, stack: err.stack}, this.prefix);
}
this.emit("stopped", addon.id);
Toasts.show(Strings.Addons.disabled.format({name: addon.name, version: addon.version}));
@ -207,4 +208,4 @@ export default new class PluginManager extends AddonManager {
}
}
}
};
};

View File

@ -13,11 +13,12 @@ const path = require("path");
export default new class ThemeManager extends AddonManager {
get name() {return "ThemeManager";}
get extension() {return ".theme.css";}
get duplicatePattern() {return /\.theme\s?\([0-9]+\)\.css/;}
get extension() {return [".theme.css", ".theme.zip"];}
get duplicatePattern() {return /\.theme\s?\([0-9]+\)\.(css|zip)/;}
get addonFolder() {return path.resolve(Config.dataPath, "themes");}
get prefix() {return "theme";}
get language() {return "css";}
get langExtension() {return ".css";}
initialize() {
const errors = super.initialize();
@ -85,4 +86,4 @@ export default new class ThemeManager extends AddonManager {
DOMManager.removeTheme(addon.slug + "-theme-container");
Toasts.show(Strings.Addons.disabled.format({name: addon.name, version: addon.version}));
}
};
};

View File

@ -148,7 +148,7 @@ export default function AddonList({prefix, type, title, folder, addonList, addon
return sorted.map(addon => {
const hasSettings = addon.instance && typeof(addon.instance.getSettingsPanel) === "function";
const getSettings = hasSettings && addon.instance.getSettingsPanel.bind(addon.instance);
return <ErrorBoundary><AddonCard disabled={addon.partial} type={type} editAddon={() => triggerEdit(addon.id)} deleteAddon={() => triggerDelete(addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>;
return <ErrorBoundary><AddonCard disabled={addon.partial} type={type} editAddon={!addon.zip && (() => triggerEdit(addon.id))} deleteAddon={() => triggerDelete(addon.id)} key={addon.id} enabled={addonState[addon.id]} addon={addon} onChange={onChange} reload={reload} hasSettings={hasSettings} getSettingsPanel={getSettings} /></ErrorBoundary>;
});
}, [addonList, addonState, onChange, reload, triggerDelete, triggerEdit, type, sort, ascending, query, forced]); // eslint-disable-line react-hooks/exhaustive-deps

View File

@ -30,7 +30,8 @@ module.exports = {
modules$: path.resolve("src", "modules"),
data$: path.resolve("src", "modules"),
builtins$: path.resolve("src", "modules"),
common: path.resolve(__dirname, "..", "common")
common: path.resolve(__dirname, "..", "common"),
"fflate": require.resolve("fflate")
}
},
module: {
@ -64,4 +65,4 @@ module.exports = {
})
]
}
};
};