owned this note
owned this note
Published
Linked with GitHub
[Electron]: https://www.electronjs.org/
[BrowserWindow]: https://www.electronjs.org/docs/latest/api/browser-window
[contextBridge.exposeInMainWorld]: https://www.electronjs.org/docs/latest/api/context-bridge#contextbridgeexposeinmainworldapikey-api
[mime-types]: https://www.npmjs.com/package/mime-types
[semantic version 2.0.0]: https://semver.org/
# Replugged Rewrite Draft
:::warning
This is just a draft and is in no way a set-in-stone description of Replugged's rewrite.
:::
## Table of Contents
- [Contexts](#Contexts)
- [`main`](#main)
- [`preload`](#preload)
- [`renderer`](#renderer)
- [Plugins](#Plugins)
- [Specification](#Specification)
- [Template](#Template)
## Contexts
### `main`
- Bootstrapper
- This is created by the installer
- This does NOT (functionally) live in the installation, and instead lives in `Discord/app/resources/app/index.js`
- Will check the replugged installation for plugs, unversioned takes priority over versioned
- Looks for `{plug}/main.js` or `{plug}/build/main.js`
- Where the main `Replugged` instance will live.
- Where the [Electron] [BrowserWindow] patch will live and be used.
```ts
import electron, { BrowserWindow, BrowserWindowConstructorOptions } from "electron";
export const ProxiedWindow = new Proxy(BrowserWindow, {
construct(target, [windowOptions]: [BrowserWindowConstructorOptions], _) {
}
});
// Ideally we'd be transpiling from ESM to CJS instead of writing in CJS, so require is possible.
const ELECTRON_PATH = require.resolve("electron");
// This is a getter so we cannot overwrite it.
delete require.cache[ELECTRON_PATH].exports;
require.cache[ELECTRON_PATH].exports = { ...electron, BrowserWindow: ProxiedWindow };
// Run original app
```
- Create a replugged scheme for serving files
```ts
import { app, protocol } from "electron";
protocol.registerSchemesAsPrivileged([{
scheme: "replugged",
privileges: {
standard: true,
secure: true,
bypassCSP: true,
allowServiceWorkers: true,
supportFetchAPI: true,
corsEnabled: true,
},
}]);
app.whenReady().then(() => {
protocol.registerBufferProtocol("replugged", (request, callback) => {
if (request.method !== "GET") return
// Get file from preload
replugged.protocol.get(request.url).then(file => {
callback({ ...file, data: Buffer.from(file.data) })
})
})
});
```
### `preload`
- Where the `RepluggedBridge` instance will live.
- Creates and exposes `RepluggedNative` with [contextBridge.exposeInMainWorld]
- Injects `RepluggedRenderer` into renderer DOM
> ```ts
> const scriptElem = Object.assign(
> document.createElement("script"),
> { type: "module", src: "replugged://base/renderer.js" },
> );
>
> // Doesn't have to be .documentElement
> while (!document.documentElement)
> await new Promise(resolve => setImmediate(resolve));
>
> document.documentElement.appendChild(scriptElem);
> scriptElem.remove();
> ```
- Bridges the IPC between [renderer](#renderer) and [main](#main) contexts
### `renderer`
- Where the `RepluggedNative` global will live.
- Where the `replugged: RepluggedRenderer` global will live.
## Plugins
- Lifecycle
- States
- `init` --- The manifest file was read and a plugin instance was created.
- `preload` --- The preload script is was loaded
- `renderer` --- The renderer script was loaded
- `idle` --- plugin never started, `plugin.stop` resolved, or `plugin.start` threw
- `starting` --- `plugin.start` was called
- `running` --- `plugin.start` resolved
- `stopping` --- `plugin.stop` was called
- `unloadable` --- There was an error loading the plugin
- Hooks
- Can be registered globally or for specific plugins.
- `afterInit`
- `beforePreload`
- `afterPreload`
- `beforeRenderer`
- `afterRenderer`
- `beforeStart`
- `afterStart`
- `beforeStop`
- `afterStop`
- `onError`
- File Structure
- `{...entries}` The entry files, `renderer` entries will run in `postPreload`
- `manifest.json` A [Plugin Manifest](#plugin-manifest) containing information about the plugin
### Specification
#### `Replugged`
- `protocol: Protocol`
- `ipc: IpcMain`
- `transpilers: TranspilerManager`
- `settings: SettingsManager`
- `plugins: PluginManager`
---
#### `RepluggedBridge` Instance
- `ipc: IpcBridge`
- `plugins: IpcBridge`
#### `Protocol` Instance
- `setFile(virtualPath: string, filePath: string)`
- Will register `GET {virtualPath}` as a static endpoint, serving `filePath` (MIME type from [mime-types])
- Example:
```ts
protocol.setFile(
"plugins/vpc-shiki/shiki.worker.js",
plugins.get("vpc-shiki").resolvePath("shiki.worker.js"),
);
// in renderer
fetch("replugged://plugins/vpc-shiki/shiki.worker.js");
```
- `[method](handlePath: string)`
- Example protocol handles
- `replugged://`
- `renderer.js`
- `themes/`
- `quickcss.css`
- `plugins/`
- `vpc-shiki/`
- `index.js`
- `oniguruma.wasm`
- `shiki.worker.js`
#### `PluginManager` Instance
- `get(id: string): ManagedPlugin`
#### `ManagedPlugin` Instance
- `manifest: Manifest`
- `resolvePath(...paths: string[])`
#### `SettingsManager` Instance
#### `TranspilerManager` Instance
- `set(key: string, type: 'js' | 'css', extensions: string[], transpileFn: (path: string) => Buffer)`
- `remove(key: string)`
---
#### `window.RepluggedNative`
- `settings`
- `logger`
- This could exist in `replugged` but would be duplicate code
- `ipc`
- `on(key: string, channel: string, callback: Function)`
- Listen to messages from `channel`
- `once(channel: string, callback: Function)`
- `off(key: string)`
- `send(channel: string, ...args: any[])`
- `call(channel: string, ...args: any[]): Promise<any>`
#### `RepluggedRenderer` instance (`window.replugged`)
- `style: StyleManager`
- `set(key: string, styleSource: string)`
- `remove(key: string)`
- `webpack`
- `components`
- `commands`
- `rpc`
- `injector`
- `before(key: string, target: object, property: string, callback: BeforeCallback)`
- `PatchCallback: (args: any[], originalFn: Callable) => any`
- `instead(key: string, target: object, property: string, callback: BeforeCallback)`
- `after(key: string, `
- *All callbacks will be bound to the `this` of the original*
- `ipc`
- `create(namespace: string): NamespacedIpc`
#### `NamespacedIpc`
- `on(eventName: string, callback: Function)`
- `once(key: string, callback: Function)`
- `on(eventName: string, callback: Function)`
- `send(channel: string, ...args: any[])`
- `call(channel: string, ...args: any[]): Promise<any>`
---
#### Plugin Manifest
- `name`
- The name of the Plugin to display to the user.
- `string`
- `id`
- The ID of the plugin to be used behind the scenes, and for dependencies.
- `string`
- `version`
- Version of the plugin using the [semantic version 2.0.0] specification.
- `string`
- `author`
- Author of the plugin, optionally including URL or email.
- `string | string[3] | { name: string, url: string, email: string }`
- `entries`
- Paths to script entry-points relative to the plugin's folder.
- `{ renderer: string | string[], main: string | string[] }`
- `repository`
- Repository link, can be used to update the plugin.
- `string`
- `priority`
- Used to determine instantiation order among siblings in the dependency tree
- `number`
- Default, `0`.
- `dependencies`
- A list of dependencies that must be loaded prior to the plugin specifying them.
- As a `string`, a [semantic version 2.0.0] specification.
- Default, unversioned.
- As a `boolean`, whether the dependency is required.
- Default, `true`.
- As an object, an optional `required` key as per `boolean`, an optional `version` key as per `string`, and an optional `repository` key as per `string`.
- Repository links can be used to allow the user to easily resolve a dependency
- *Circular dependency resolution is up to somebody else.*
- `{ [id: string]: string | boolean | { required: boolean, version: string, repository: string } }`
### Installation File Structure
- `plugins`
- `themes`
- `settings`
- `(plug|plug-x.x.x)(.asar)?` Unversioned takes priority over versioned
- `main.js`
- `preload.js`
- `renderer.js`
- `package.json`
### Template
### Coremods
#### Themes
#### Updater
- Checks if plug is versioned
- **Versioned** --- `production` environment
- **Unversioned** --- `development` environment
- Automatic Updates
- `production` --- default: enabled
- `development` --- default: disabled, show warning on enable (will overwrite files)
#### LegacyCompat