Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Native Api #656

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
46 changes: 46 additions & 0 deletions bin/index.mts
Original file line number Diff line number Diff line change
Expand Up @@ -332,15 +332,46 @@ async function buildPlugin({ watch, noInstall, production, noReload, addon }: Ar
};
}

if (args.importer?.startsWith?.(path.resolve(manifest.native))) {
return {
errors: [
{
text: `Unsupported import: ${args.path}\nOnly Native lib imports are supported in native files.`,
},
],
};
}
return {
path: args.path,
namespace: "replugged",
};
});

build.onResolve(
{ filter: new RegExp(`.*?/${manifest.native?.split("/")?.pop()?.replace(/\..+/, "")}`) },
(args) => {
if (args.kind !== "import-statement") return undefined;

return {
path: `replugged/plugins/pluginNatives.get("${manifest.id}")`,
namespace: "replugged",
};
},
);

build.onResolve({ filter: /^react$/ }, (args) => {
if (args.kind !== "import-statement") return undefined;

if (args.importer?.startsWith?.(path.resolve(manifest.native))) {
return {
errors: [
{
text: `Unsupported import: ${args.path}\nOnly Native lib imports are supported in native files.`,
},
],
};
}

return {
path: "replugged/common/React",
namespace: "replugged",
Expand Down Expand Up @@ -427,6 +458,21 @@ async function buildPlugin({ watch, noInstall, production, noReload, addon }: Ar

manifest.plaintextPatches = "plaintextPatches.js";
}
if ("native" in manifest) {
targets.push(
esbuild.context(
overwrites({
...common,
platform: "node",
format: "cjs",
entryPoints: [path.join(folderPath, manifest.native)],
outfile: `${distPath}/native.js`,
}),
),
);

manifest.native = "native.js";
}

if (!existsSync(distPath)) mkdirSync(distPath, { recursive: true });

Expand Down
64 changes: 52 additions & 12 deletions src/main/ipc/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import { extname, join, sep } from "path";
import { ipcMain, shell } from "electron";
import { RepluggedIpcChannels, type RepluggedPlugin } from "../../types";
import { plugin } from "../../types/addon";
import type { Dirent, Stats } from "fs";
import { type Dirent, type Stats } from "fs";
import { CONFIG_PATHS } from "src/util.mjs";

import { getSetting } from "./settings";
const PLUGINS_DIR = CONFIG_PATHS.plugins;

export const isFileAPlugin = (f: Dirent | Stats, name: string): boolean => {
Expand Down Expand Up @@ -49,16 +49,7 @@ async function getPlugin(pluginName: string): Promise<RepluggedPlugin> {
return data;
}

ipcMain.handle(
RepluggedIpcChannels.GET_PLUGIN,
async (_, pluginName: string): Promise<RepluggedPlugin | undefined> => {
try {
return await getPlugin(pluginName);
} catch {}
},
);

ipcMain.handle(RepluggedIpcChannels.LIST_PLUGINS, async (): Promise<RepluggedPlugin[]> => {
async function listPlugins(): Promise<RepluggedPlugin[]> {
const plugins = [];

const pluginDirs = (
Expand Down Expand Up @@ -88,8 +79,57 @@ ipcMain.handle(RepluggedIpcChannels.LIST_PLUGINS, async (): Promise<RepluggedPlu
}

return plugins;
}

ipcMain.handle(
RepluggedIpcChannels.GET_PLUGIN,
async (_, pluginName: string): Promise<RepluggedPlugin | undefined> => {
try {
return await getPlugin(pluginName);
} catch {}
},
);

ipcMain.handle(RepluggedIpcChannels.LIST_PLUGINS, async (): Promise<RepluggedPlugin[]> => {
return await listPlugins();
});

ipcMain.handle(
RepluggedIpcChannels.LIST_PLUGINS_NATIVE,
async (): Promise<Record<string, Record<string, string>>> => {
const disabled = await getSetting<string[]>("plugins", "disabled", []);
const plugins = await listPlugins();

return plugins.reduce((acc: Record<string, Record<string, string>>, plugin) => {
try {
if (!plugin.manifest.native || disabled.includes(plugin.manifest.id)) return acc;
const nativePath = join(PLUGINS_DIR, plugin.path, plugin.manifest.native);
if (!nativePath.startsWith(`${PLUGINS_DIR}${sep}`)) {
// Ensure file changes are restricted to the base path
throw new Error("Invalid plugin name");
}

const entries = Object.entries<(...args: unknown[]) => unknown>(require(nativePath));
if (!entries.length) return acc;

const mappings: Record<string, string> = {};

for (const [methodName, method] of entries) {
const key = `Replugged_Plugin_Native_[${plugin.manifest.id}]_${methodName}`;
ipcMain.handle(key, (_, ...args) => method(...args) /* For easy type when importing */);
mappings[methodName] = key;
}
acc[plugin.manifest.id] = mappings;
return acc;
} catch (e) {
console.error(`Error Loading Plugin Native`, plugin.manifest);
console.error(e);
return acc;
}
}, {});
},
);

ipcMain.handle(RepluggedIpcChannels.UNINSTALL_PLUGIN, async (_, pluginName: string) => {
const pluginPath = join(PLUGINS_DIR, pluginName);
if (!pluginPath.startsWith(`${PLUGINS_DIR}${sep}`)) {
Expand Down
21 changes: 21 additions & 0 deletions src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
InstallResultFailure,
InstallResultSuccess,
InstallerType,
PluginNativeMap,
RepluggedPlugin,
RepluggedTheme,
} from "./types";
Expand All @@ -22,6 +23,24 @@ void ipcRenderer.invoke(RepluggedIpcChannels.GET_REPLUGGED_VERSION).then((v) =>
version = v;
});

const mapNative = (
nativeList: Record<string, Record<string, string>>,
): Array<[string, PluginNativeMap]> => {
const pluginNatives = {} as Record<string, PluginNativeMap>;
for (const pluginId in nativeList) {
const methods = nativeList[pluginId];
const map = {} as Record<string, (...args: unknown[]) => Promise<unknown>>;
for (const methodName in methods) {
map[methodName] = (...args: unknown[]) =>
ipcRenderer.invoke(methods[methodName], ...args).catch((err) => {
throw new Error(err.message.split(": Error: ")[1]);
});
}
pluginNatives[pluginId] = map;
}
return Object.entries(pluginNatives);
};

const RepluggedNative = {
themes: {
list: async (): Promise<RepluggedTheme[]> =>
Expand All @@ -36,6 +55,8 @@ const RepluggedNative = {
ipcRenderer.invoke(RepluggedIpcChannels.GET_PLUGIN, pluginPath),
list: async (): Promise<RepluggedPlugin[]> =>
ipcRenderer.invoke(RepluggedIpcChannels.LIST_PLUGINS),
listNative: async (): Promise<Array<[string, PluginNativeMap]>> =>
ipcRenderer.invoke(RepluggedIpcChannels.LIST_PLUGINS_NATIVE).then(mapNative),
uninstall: async (pluginPath: string): Promise<RepluggedPlugin> =>
ipcRenderer.invoke(RepluggedIpcChannels.UNINSTALL_PLUGIN, pluginPath),
openFolder: () => ipcRenderer.send(RepluggedIpcChannels.OPEN_PLUGINS_FOLDER),
Expand Down
9 changes: 9 additions & 0 deletions src/renderer/managers/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface PluginWrapper extends RepluggedPlugin {
/**
* @hidden
*/
export const pluginNatives = new Map<string, Record<string, (...args: unknown[]) => unknown>>();
export const plugins = new Map<string, PluginWrapper>();
const running = new Set<string>();

Expand Down Expand Up @@ -48,6 +49,13 @@ function register(plugin: RepluggedPlugin): void {
});
}

function registerNative([name, map]: [
string,
Record<string, (...args: unknown[]) => unknown>,
]): void {
pluginNatives.set(name, map);
}

/**
* Load all plugins
*
Expand All @@ -56,6 +64,7 @@ function register(plugin: RepluggedPlugin): void {
*/
export async function loadAll(): Promise<void> {
(await window.RepluggedNative.plugins.list()).forEach(register);
(await RepluggedNative.plugins.listNative()).forEach(registerNative);
}

/**
Expand Down
3 changes: 3 additions & 0 deletions src/types/addon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const plugin = common.extend({
type: z.literal("replugged-plugin"),
renderer: z.string().optional(),
plaintextPatches: z.string().optional(),
native: z.string().optional(),
reloadRequired: z.boolean().optional(),
});

Expand All @@ -82,6 +83,8 @@ export interface PluginExports {
[x: string]: unknown;
}

export type PluginNativeMap = Record<string, (...args: unknown[]) => Promise<unknown>>;

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type AddonSettings = {
disabled?: string[];
Expand Down
9 changes: 8 additions & 1 deletion src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export enum RepluggedIpcChannels {
GET_THEME = "REPLUGGED_GET_THEME",
UNINSTALL_THEME = "REPLUGGED_UNINSTALL_THEME",
LIST_PLUGINS = "REPLUGGED_LIST_PLUGINS",
LIST_PLUGINS_NATIVE = "REPLUGGED_LIST_PLUGINS_NATIVE",
GET_PLUGIN = "REPLUGGED_GET_PLUGIN",
UNINSTALL_PLUGIN = "REPLUGGED_UNINSTALL_PLUGIN",
REGISTER_RELOAD = "REPLUGGED_REGISTER_RELOAD",
Expand Down Expand Up @@ -73,7 +74,13 @@ export interface RepluggedPlugin {
hasCSS: boolean;
}

export type { AnyAddonManifest, PluginExports, PluginManifest, ThemeManifest } from "./addon";
export type {
AnyAddonManifest,
PluginExports,
PluginNativeMap,
PluginManifest,
ThemeManifest,
} from "./addon";
export * from "./coremods/commands";
export * from "./coremods/contextMenu";
export * from "./coremods/message";
Expand Down
Loading