boilerplate for express server

This commit is contained in:
Linh Tran 2023-12-01 09:35:51 +07:00
parent 64a58d1e15
commit e5f57b853b
28 changed files with 90 additions and 1374 deletions

View File

@ -1,30 +0,0 @@
const { ipcRenderer, contextBridge } = require("electron");
export function useFacade() {
const interfaces = {
install(plugins: any[]) {
return ipcRenderer.invoke("pluggable:install", plugins);
},
uninstall(plugins: any[], reload: boolean) {
return ipcRenderer.invoke("pluggable:uninstall", plugins, reload);
},
getActive() {
return ipcRenderer.invoke("pluggable:getActivePlugins");
},
update(plugins: any[], reload: boolean) {
return ipcRenderer.invoke("pluggable:update", plugins, reload);
},
updatesAvailable(plugin: any) {
return ipcRenderer.invoke("pluggable:updatesAvailable", plugin);
},
toggleActive(plugin: any, active: boolean) {
return ipcRenderer.invoke("pluggable:togglePluginActive", plugin, active);
},
};
if (contextBridge) {
contextBridge.exposeInMainWorld("pluggableElectronIpc", interfaces);
}
return interfaces;
}

View File

@ -1,36 +0,0 @@
import { existsSync, mkdirSync, writeFileSync } from "fs";
import { join, resolve } from "path";
export let pluginsPath: string | undefined = undefined;
/**
* @private
* Set path to plugins directory and create the directory if it does not exist.
* @param {string} plgPath path to plugins directory
*/
export function setPluginsPath(plgPath: string) {
// Create folder if it does not exist
let plgDir;
try {
plgDir = resolve(plgPath);
if (plgDir.length < 2) throw new Error();
if (!existsSync(plgDir)) mkdirSync(plgDir);
const pluginsJson = join(plgDir, "plugins.json");
if (!existsSync(pluginsJson)) writeFileSync(pluginsJson, "{}", "utf8");
pluginsPath = plgDir;
} catch (error) {
throw new Error("Invalid path provided to the plugins folder");
}
}
/**
* @private
* Get the path to the plugins.json file.
* @returns location of plugins.json
*/
export function getPluginsFile() {
return join(pluginsPath ?? "", "plugins.json");
}

View File

@ -1,149 +0,0 @@
import { readFileSync } from "fs";
import { protocol } from "electron";
import { normalize } from "path";
import Plugin from "./plugin";
import {
getAllPlugins,
removePlugin,
persistPlugins,
installPlugins,
getPlugin,
getActivePlugins,
addPlugin,
} from "./store";
import {
pluginsPath as storedPluginsPath,
setPluginsPath,
getPluginsFile,
} from "./globals";
import router from "./router";
/**
* Sets up the required communication between the main and renderer processes.
* Additionally sets the plugins up using {@link usePlugins} if a pluginsPath is provided.
* @param {Object} options configuration for setting up the renderer facade.
* @param {confirmInstall} [options.confirmInstall] Function to validate that a plugin should be installed.
* @param {Boolean} [options.useFacade=true] Whether to make a facade to the plugins available in the renderer.
* @param {string} [options.pluginsPath] Optional path to the plugins folder.
* @returns {pluginManager|Object} A set of functions used to manage the plugin lifecycle if usePlugins is provided.
* @function
*/
export function init(options: any) {
if (
!Object.prototype.hasOwnProperty.call(options, "useFacade") ||
options.useFacade
) {
// Enable IPC to be used by the facade
router();
}
// Create plugins protocol to serve plugins to renderer
registerPluginProtocol();
// perform full setup if pluginsPath is provided
if (options.pluginsPath) {
return usePlugins(options.pluginsPath);
}
return {};
}
/**
* Create plugins protocol to provide plugins to renderer
* @private
* @returns {boolean} Whether the protocol registration was successful
*/
function registerPluginProtocol() {
return protocol.registerFileProtocol("plugin", (request, callback) => {
const entry = request.url.substr(8);
const url = normalize(storedPluginsPath + entry);
callback({ path: url });
});
}
/**
* Set Pluggable Electron up to run from the pluginPath folder if it is provided and
* load plugins persisted in that folder.
* @param {string} pluginsPath Path to the plugins folder. Required if not yet set up.
* @returns {pluginManager} A set of functions used to manage the plugin lifecycle.
*/
export function usePlugins(pluginsPath: string) {
if (!pluginsPath)
throw Error(
"A path to the plugins folder is required to use Pluggable Electron"
);
// Store the path to the plugins folder
setPluginsPath(pluginsPath);
// Remove any registered plugins
for (const plugin of getAllPlugins()) {
if (plugin.name) removePlugin(plugin.name, false);
}
// Read plugin list from plugins folder
const plugins = JSON.parse(readFileSync(getPluginsFile(), "utf-8"));
try {
// Create and store a Plugin instance for each plugin in list
for (const p in plugins) {
loadPlugin(plugins[p]);
}
persistPlugins();
} catch (error) {
// Throw meaningful error if plugin loading fails
throw new Error(
"Could not successfully rebuild list of installed plugins.\n" +
error +
"\nPlease check the plugins.json file in the plugins folder."
);
}
// Return the plugin lifecycle functions
return getStore();
}
/**
* Check the given plugin object. If it is marked for uninstalling, the plugin files are removed.
* Otherwise a Plugin instance for the provided object is created and added to the store.
* @private
* @param {Object} plg Plugin info
*/
function loadPlugin(plg: any) {
// Create new plugin, populate it with plg details and save it to the store
const plugin = new Plugin();
for (const key in plg) {
if (Object.prototype.hasOwnProperty.call(plg, key)) {
// Use Object.defineProperty to set the properties as writable
Object.defineProperty(plugin, key, {
value: plg[key],
writable: true,
enumerable: true,
configurable: true,
});
}
}
addPlugin(plugin, false);
plugin.subscribe("pe-persist", persistPlugins);
}
/**
* Returns the publicly available store functions.
* @returns {pluginManager} A set of functions used to manage the plugin lifecycle.
*/
export function getStore() {
if (!storedPluginsPath) {
throw new Error(
"The plugin path has not yet been set up. Please run usePlugins before accessing the store"
);
}
return {
installPlugins,
getPlugin,
getAllPlugins,
getActivePlugins,
removePlugin,
};
}

View File

@ -1,213 +0,0 @@
import { rmdir } from "fs/promises";
import { resolve, join } from "path";
import { manifest, extract } from "pacote";
import * as Arborist from "@npmcli/arborist";
import { pluginsPath } from "./globals";
/**
* An NPM package that can be used as a Pluggable Electron plugin.
* Used to hold all the information and functions necessary to handle the plugin lifecycle.
*/
class Plugin {
/**
* @property {string} origin Original specification provided to fetch the package.
* @property {Object} installOptions Options provided to pacote when fetching the manifest.
* @property {name} name The name of the plugin as defined in the manifest.
* @property {string} url Electron URL where the package can be accessed.
* @property {string} version Version of the package as defined in the manifest.
* @property {Array<string>} activationPoints List of {@link ./Execution-API#activationPoints|activation points}.
* @property {string} main The entry point as defined in the main entry of the manifest.
* @property {string} description The description of plugin as defined in the manifest.
* @property {string} icon The icon of plugin as defined in the manifest.
*/
origin?: string;
installOptions: any;
name?: string;
url?: string;
version?: string;
activationPoints?: Array<string>;
main?: string;
description?: string;
icon?: string;
/** @private */
_active = false;
/**
* @private
* @property {Object.<string, Function>} #listeners A list of callbacks to be executed when the Plugin is updated.
*/
listeners: Record<string, (obj: any) => void> = {};
/**
* Set installOptions with defaults for options that have not been provided.
* @param {string} [origin] Original specification provided to fetch the package.
* @param {Object} [options] Options provided to pacote when fetching the manifest.
*/
constructor(origin?: string, options = {}) {
const defaultOpts = {
version: false,
fullMetadata: false,
Arborist,
};
this.origin = origin;
this.installOptions = { ...defaultOpts, ...options };
}
/**
* Package name with version number.
* @type {string}
*/
get specifier() {
return (
this.origin +
(this.installOptions.version ? "@" + this.installOptions.version : "")
);
}
/**
* Whether the plugin should be registered with its activation points.
* @type {boolean}
*/
get active() {
return this._active;
}
/**
* Set Package details based on it's manifest
* @returns {Promise.<Boolean>} Resolves to true when the action completed
*/
async getManifest() {
// Get the package's manifest (package.json object)
try {
const mnf = await manifest(this.specifier, this.installOptions);
// set the Package properties based on the it's manifest
this.name = mnf.name;
this.version = mnf.version;
this.activationPoints = mnf.activationPoints
? (mnf.activationPoints as string[])
: undefined;
this.main = mnf.main;
this.description = mnf.description;
this.icon = mnf.icon as any;
} catch (error) {
throw new Error(
`Package ${this.origin} does not contain a valid manifest: ${error}`
);
}
return true;
}
/**
* Extract plugin to plugins folder.
* @returns {Promise.<Plugin>} This plugin
* @private
*/
async _install() {
try {
// import the manifest details
await this.getManifest();
// Install the package in a child folder of the given folder
await extract(
this.specifier,
join(pluginsPath ?? "", this.name ?? ""),
this.installOptions
);
if (!Array.isArray(this.activationPoints))
throw new Error("The plugin does not contain any activation points");
// Set the url using the custom plugins protocol
this.url = `plugin://${this.name}/${this.main}`;
this.emitUpdate();
} catch (err) {
// Ensure the plugin is not stored and the folder is removed if the installation fails
this.setActive(false);
throw err;
}
return [this];
}
/**
* Subscribe to updates of this plugin
* @param {string} name name of the callback to register
* @param {callback} cb The function to execute on update
*/
subscribe(name: string, cb: () => void) {
this.listeners[name] = cb;
}
/**
* Remove subscription
* @param {string} name name of the callback to remove
*/
unsubscribe(name: string) {
delete this.listeners[name];
}
/**
* Execute listeners
*/
emitUpdate() {
for (const cb in this.listeners) {
this.listeners[cb].call(null, this);
}
}
/**
* Check for updates and install if available.
* @param {string} version The version to update to.
* @returns {boolean} Whether an update was performed.
*/
async update(version = false) {
if (await this.isUpdateAvailable()) {
this.installOptions.version = version;
await this._install();
return true;
}
return false;
}
/**
* Check if a new version of the plugin is available at the origin.
* @returns the latest available version if a new version is available or false if not.
*/
async isUpdateAvailable() {
if (this.origin) {
const mnf = await manifest(this.origin);
return mnf.version !== this.version ? mnf.version : false;
}
}
/**
* Remove plugin and refresh renderers.
* @returns {Promise}
*/
async uninstall() {
const plgPath = resolve(pluginsPath ?? "", this.name ?? "");
await rmdir(plgPath, { recursive: true });
this.emitUpdate();
}
/**
* Set a plugin's active state. This determines if a plugin should be loaded on initialisation.
* @param {boolean} active State to set _active to
* @returns {Plugin} This plugin
*/
setActive(active: boolean) {
this._active = active;
this.emitUpdate();
return this;
}
}
export default Plugin;

View File

@ -1,97 +0,0 @@
import { ipcMain, webContents } from "electron";
import {
getPlugin,
getActivePlugins,
installPlugins,
removePlugin,
getAllPlugins,
} from "./store";
import { pluginsPath } from "./globals";
import Plugin from "./plugin";
// Throw an error if pluginsPath has not yet been provided by usePlugins.
const checkPluginsPath = () => {
if (!pluginsPath)
throw Error("Path to plugins folder has not yet been set up.");
};
let active = false;
/**
* Provide the renderer process access to the plugins.
**/
export default function () {
if (active) return;
// Register IPC route to install a plugin
ipcMain.handle("pluggable:install", async (e, plugins) => {
checkPluginsPath();
// Install and activate all provided plugins
const installed = await installPlugins(plugins);
return JSON.parse(JSON.stringify(installed));
});
// Register IPC route to uninstall a plugin
ipcMain.handle("pluggable:uninstall", async (e, plugins, reload) => {
checkPluginsPath();
// Uninstall all provided plugins
for (const plg of plugins) {
const plugin = getPlugin(plg);
await plugin.uninstall();
if (plugin.name) removePlugin(plugin.name);
}
// Reload all renderer pages if needed
reload && webContents.getAllWebContents().forEach((wc) => wc.reload());
return true;
});
// Register IPC route to update a plugin
ipcMain.handle("pluggable:update", async (e, plugins, reload) => {
checkPluginsPath();
// Update all provided plugins
const updated: Plugin[] = [];
for (const plg of plugins) {
const plugin = getPlugin(plg);
const res = await plugin.update();
if (res) updated.push(plugin);
}
// Reload all renderer pages if needed
if (updated.length && reload)
webContents.getAllWebContents().forEach((wc) => wc.reload());
return JSON.parse(JSON.stringify(updated));
});
// Register IPC route to check if updates are available for a plugin
ipcMain.handle("pluggable:updatesAvailable", (e, names) => {
checkPluginsPath();
const plugins = names
? names.map((name: string) => getPlugin(name))
: getAllPlugins();
const updates: Record<string, Plugin> = {};
for (const plugin of plugins) {
updates[plugin.name] = plugin.isUpdateAvailable();
}
return updates;
});
// Register IPC route to get the list of active plugins
ipcMain.handle("pluggable:getActivePlugins", () => {
checkPluginsPath();
return JSON.parse(JSON.stringify(getActivePlugins()));
});
// Register IPC route to toggle the active state of a plugin
ipcMain.handle("pluggable:togglePluginActive", (e, plg, active) => {
checkPluginsPath();
const plugin = getPlugin(plg);
return JSON.parse(JSON.stringify(plugin.setActive(active)));
});
active = true;
}

View File

@ -1,131 +0,0 @@
/**
* Provides access to the plugins stored by Pluggable Electron
* @typedef {Object} pluginManager
* @prop {getPlugin} getPlugin
* @prop {getAllPlugins} getAllPlugins
* @prop {getActivePlugins} getActivePlugins
* @prop {installPlugins} installPlugins
* @prop {removePlugin} removePlugin
*/
import { writeFileSync } from "fs";
import Plugin from "./plugin";
import { getPluginsFile } from "./globals";
/**
* @module store
* @private
*/
/**
* Register of installed plugins
* @type {Object.<string, Plugin>} plugin - List of installed plugins
*/
const plugins: Record<string, Plugin> = {};
/**
* Get a plugin from the stored plugins.
* @param {string} name Name of the plugin to retrieve
* @returns {Plugin} Retrieved plugin
* @alias pluginManager.getPlugin
*/
export function getPlugin(name: string) {
if (!Object.prototype.hasOwnProperty.call(plugins, name)) {
throw new Error(`Plugin ${name} does not exist`);
}
return plugins[name];
}
/**
* Get list of all plugin objects.
* @returns {Array.<Plugin>} All plugin objects
* @alias pluginManager.getAllPlugins
*/
export function getAllPlugins() {
return Object.values(plugins);
}
/**
* Get list of active plugin objects.
* @returns {Array.<Plugin>} Active plugin objects
* @alias pluginManager.getActivePlugins
*/
export function getActivePlugins() {
return Object.values(plugins).filter((plugin) => plugin.active);
}
/**
* Remove plugin from store and maybe save stored plugins to file
* @param {string} name Name of the plugin to remove
* @param {boolean} persist Whether to save the changes to plugins to file
* @returns {boolean} Whether the delete was successful
* @alias pluginManager.removePlugin
*/
export function removePlugin(name: string, persist = true) {
const del = delete plugins[name];
if (persist) persistPlugins();
return del;
}
/**
* Add plugin to store and maybe save stored plugins to file
* @param {Plugin} plugin Plugin to add to store
* @param {boolean} persist Whether to save the changes to plugins to file
* @returns {void}
*/
export function addPlugin(plugin: Plugin, persist = true) {
if (plugin.name) plugins[plugin.name] = plugin;
if (persist) {
persistPlugins();
plugin.subscribe("pe-persist", persistPlugins);
}
}
/**
* Save stored plugins to file
* @returns {void}
*/
export function persistPlugins() {
const persistData: Record<string, Plugin> = {};
for (const name in plugins) {
persistData[name] = plugins[name];
}
writeFileSync(getPluginsFile(), JSON.stringify(persistData), "utf8");
}
/**
* Create and install a new plugin for the given specifier.
* @param {Array.<installOptions | string>} plugins A list of NPM specifiers, or installation configuration objects.
* @param {boolean} [store=true] Whether to store the installed plugins in the store
* @returns {Promise.<Array.<Plugin>>} New plugin
* @alias pluginManager.installPlugins
*/
export async function installPlugins(plugins: any, store = true) {
const installed: Plugin[] = [];
for (const plg of plugins) {
// Set install options and activation based on input type
const isObject = typeof plg === "object";
const spec = isObject ? [plg.specifier, plg] : [plg];
const activate = isObject ? plg.activate !== false : true;
// Install and possibly activate plugin
const plugin = new Plugin(...spec);
await plugin._install();
if (activate) plugin.setActive(true);
// Add plugin to store if needed
if (store) addPlugin(plugin);
installed.push(plugin);
}
// Return list of all installed plugins
return installed;
}
/**
* @typedef {Object.<string, any>} installOptions The {@link https://www.npmjs.com/package/pacote|pacote}
* options used to install the plugin with some extra options.
* @param {string} specifier the NPM specifier that identifies the package.
* @param {boolean} [activate] Whether this plugin should be activated after installation. Defaults to true.
*/

View File

@ -1,108 +0,0 @@
import { app, ipcMain } from 'electron'
import { DownloadManager } from '../managers/download'
import { resolve, join } from 'path'
import { WindowManager } from '../managers/window'
import request from 'request'
import { createWriteStream, unlink } from 'fs'
const progress = require('request-progress')
export function handleDownloaderIPCs() {
/**
* Handles the "pauseDownload" IPC message by pausing the download associated with the provided fileName.
* @param _event - The IPC event object.
* @param fileName - The name of the file being downloaded.
*/
ipcMain.handle('pauseDownload', async (_event, fileName) => {
DownloadManager.instance.networkRequests[fileName]?.pause()
})
/**
* Handles the "resumeDownload" IPC message by resuming the download associated with the provided fileName.
* @param _event - The IPC event object.
* @param fileName - The name of the file being downloaded.
*/
ipcMain.handle('resumeDownload', async (_event, fileName) => {
DownloadManager.instance.networkRequests[fileName]?.resume()
})
/**
* Handles the "abortDownload" IPC message by aborting the download associated with the provided fileName.
* The network request associated with the fileName is then removed from the networkRequests object.
* @param _event - The IPC event object.
* @param fileName - The name of the file being downloaded.
*/
ipcMain.handle('abortDownload', async (_event, fileName) => {
const rq = DownloadManager.instance.networkRequests[fileName]
DownloadManager.instance.networkRequests[fileName] = undefined
const userDataPath = app.getPath('userData')
const fullPath = join(userDataPath, fileName)
rq?.abort()
let result = 'NULL'
unlink(fullPath, function (err) {
if (err && err.code == 'ENOENT') {
result = `File not exist: ${err}`
} else if (err) {
result = `File delete error: ${err}`
} else {
result = 'File deleted successfully'
}
console.debug(
`Delete file ${fileName} from ${fullPath} result: ${result}`
)
})
})
/**
* Downloads a file from a given URL.
* @param _event - The IPC event object.
* @param url - The URL to download the file from.
* @param fileName - The name to give the downloaded file.
*/
ipcMain.handle('downloadFile', async (_event, url, fileName) => {
const userDataPath = join(app.getPath('home'), 'jan')
const destination = resolve(userDataPath, fileName)
const rq = request(url)
progress(rq, {})
.on('progress', function (state: any) {
WindowManager?.instance.currentWindow?.webContents.send(
'FILE_DOWNLOAD_UPDATE',
{
...state,
fileName,
}
)
})
.on('error', function (err: Error) {
WindowManager?.instance.currentWindow?.webContents.send(
'FILE_DOWNLOAD_ERROR',
{
fileName,
err,
}
)
})
.on('end', function () {
if (DownloadManager.instance.networkRequests[fileName]) {
WindowManager?.instance.currentWindow?.webContents.send(
'FILE_DOWNLOAD_COMPLETE',
{
fileName,
}
)
DownloadManager.instance.setRequest(fileName, undefined)
} else {
WindowManager?.instance.currentWindow?.webContents.send(
'FILE_DOWNLOAD_ERROR',
{
fileName,
err: 'Download cancelled',
}
)
}
})
.pipe(createWriteStream(destination))
DownloadManager.instance.setRequest(fileName, rq)
})
}

View File

@ -1,156 +0,0 @@
import { app, ipcMain } from 'electron'
import * as fs from 'fs'
import { join } from 'path'
/**
* Handles file system operations.
*/
export function handleFsIPCs() {
const userSpacePath = join(app.getPath('home'), 'jan')
/**
* Gets the path to the user data directory.
* @param event - The event object.
* @returns A promise that resolves with the path to the user data directory.
*/
ipcMain.handle(
'getUserSpace',
(): Promise<string> => Promise.resolve(userSpacePath)
)
/**
* Checks whether the path is a directory.
* @param event - The event object.
* @param path - The path to check.
* @returns A promise that resolves with a boolean indicating whether the path is a directory.
*/
ipcMain.handle('isDirectory', (_event, path: string): Promise<boolean> => {
const fullPath = join(userSpacePath, path)
return Promise.resolve(
fs.existsSync(fullPath) && fs.lstatSync(fullPath).isDirectory()
)
})
/**
* Reads a file from the user data directory.
* @param event - The event object.
* @param path - The path of the file to read.
* @returns A promise that resolves with the contents of the file.
*/
ipcMain.handle('readFile', async (event, path: string): Promise<string> => {
return new Promise((resolve, reject) => {
fs.readFile(join(userSpacePath, path), 'utf8', (err, data) => {
if (err) {
reject(err)
} else {
resolve(data)
}
})
})
})
/**
* Writes data to a file in the user data directory.
* @param event - The event object.
* @param path - The path of the file to write to.
* @param data - The data to write to the file.
* @returns A promise that resolves when the file has been written.
*/
ipcMain.handle(
'writeFile',
async (event, path: string, data: string): Promise<void> => {
return new Promise((resolve, reject) => {
fs.writeFile(join(userSpacePath, path), data, 'utf8', (err) => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
}
)
/**
* Creates a directory in the user data directory.
* @param event - The event object.
* @param path - The path of the directory to create.
* @returns A promise that resolves when the directory has been created.
*/
ipcMain.handle('mkdir', async (event, path: string): Promise<void> => {
return new Promise((resolve, reject) => {
fs.mkdir(join(userSpacePath, path), { recursive: true }, (err) => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
})
/**
* Removes a directory in the user data directory.
* @param event - The event object.
* @param path - The path of the directory to remove.
* @returns A promise that resolves when the directory is removed successfully.
*/
ipcMain.handle('rmdir', async (event, path: string): Promise<void> => {
return new Promise((resolve, reject) => {
fs.rmdir(join(userSpacePath, path), { recursive: true }, (err) => {
if (err) {
reject(err)
} else {
resolve()
}
})
})
})
/**
* Lists the files in a directory in the user data directory.
* @param event - The event object.
* @param path - The path of the directory to list files from.
* @returns A promise that resolves with an array of file names.
*/
ipcMain.handle(
'listFiles',
async (event, path: string): Promise<string[]> => {
return new Promise((resolve, reject) => {
fs.readdir(join(userSpacePath, path), (err, files) => {
if (err) {
reject(err)
} else {
resolve(files)
}
})
})
}
)
/**
* Deletes a file from the user data folder.
* @param _event - The IPC event object.
* @param filePath - The path to the file to delete.
* @returns A string indicating the result of the operation.
*/
ipcMain.handle('deleteFile', async (_event, filePath) => {
const fullPath = join(userSpacePath, filePath)
let result = 'NULL'
fs.unlink(fullPath, function (err) {
if (err && err.code == 'ENOENT') {
result = `File not exist: ${err}`
} else if (err) {
result = `File delete error: ${err}`
} else {
result = 'File deleted successfully'
}
console.debug(
`Delete file ${filePath} from ${fullPath} result: ${result}`
)
})
return result
})
}

View File

@ -1,118 +0,0 @@
import { app, ipcMain } from "electron";
import { readdirSync, rmdir, writeFileSync } from "fs";
import { ModuleManager } from "../managers/module";
import { join, extname } from "path";
import { PluginManager } from "../managers/plugin";
import { WindowManager } from "../managers/window";
import { manifest, tarball } from "pacote";
export function handlePluginIPCs() {
/**
* Invokes a function from a plugin module in main node process.
* @param _event - The IPC event object.
* @param modulePath - The path to the plugin module.
* @param method - The name of the function to invoke.
* @param args - The arguments to pass to the function.
* @returns The result of the invoked function.
*/
ipcMain.handle(
"invokePluginFunc",
async (_event, modulePath, method, ...args) => {
const module = require(
/* webpackIgnore: true */ join(
app.getPath("userData"),
"plugins",
modulePath
)
);
ModuleManager.instance.setModule(modulePath, module);
if (typeof module[method] === "function") {
return module[method](...args);
} else {
console.debug(module[method]);
console.error(`Function "${method}" does not exist in the module.`);
}
}
);
/**
* Returns the paths of the base plugins.
* @param _event - The IPC event object.
* @returns An array of paths to the base plugins.
*/
ipcMain.handle("basePlugins", async (_event) => {
const basePluginPath = join(
__dirname,
"../",
app.isPackaged
? "../../app.asar.unpacked/core/pre-install"
: "../core/pre-install"
);
return readdirSync(basePluginPath)
.filter((file) => extname(file) === ".tgz")
.map((file) => join(basePluginPath, file));
});
/**
* Returns the path to the user's plugin directory.
* @param _event - The IPC event object.
* @returns The path to the user's plugin directory.
*/
ipcMain.handle("pluginPath", async (_event) => {
return join(app.getPath("userData"), "plugins");
});
/**
* Deletes the `plugins` directory in the user data path and disposes of required modules.
* If the app is packaged, the function relaunches the app and exits.
* Otherwise, the function deletes the cached modules and sets up the plugins and reloads the main window.
* @param _event - The IPC event object.
* @param url - The URL to reload.
*/
ipcMain.handle("reloadPlugins", async (_event, url) => {
const userDataPath = app.getPath("userData");
const fullPath = join(userDataPath, "plugins");
rmdir(fullPath, { recursive: true }, function (err) {
if (err) console.error(err);
ModuleManager.instance.clearImportedModules();
// just relaunch if packaged, should launch manually in development mode
if (app.isPackaged) {
app.relaunch();
app.exit();
} else {
for (const modulePath in ModuleManager.instance.requiredModules) {
delete require.cache[
require.resolve(
join(app.getPath("userData"), "plugins", modulePath)
)
];
}
PluginManager.instance.setupPlugins();
WindowManager.instance.currentWindow?.reload();
}
});
});
/**
* Installs a remote plugin by downloading its tarball and writing it to a tgz file.
* @param _event - The IPC event object.
* @param pluginName - The name of the remote plugin to install.
* @returns A Promise that resolves to the path of the installed plugin file.
*/
ipcMain.handle("installRemotePlugin", async (_event, pluginName) => {
const destination = join(
app.getPath("userData"),
pluginName.replace(/^@.*\//, "") + ".tgz"
);
return manifest(pluginName)
.then(async (manifest: any) => {
await tarball(manifest._resolved).then((data: Buffer) => {
writeFileSync(destination, data);
});
})
.then(() => destination);
});
}

0
server/lib/.gitkeep Normal file
View File

View File

@ -1,31 +1,28 @@
import { setupMenu } from './utils/menu'
import app from 'express'
import express from 'express'
import bodyParser from 'body-parser'
import fs from 'fs'
/**
* Managers
**/
import { ModuleManager } from './managers/module'
import { PluginManager } from './managers/plugin'
import v1API from './v1'
const JAN_API_PORT = 1337;
const server = app()
server.use(bodyParser)
const server = express()
server.use(bodyParser.urlencoded())
server.use(bodyParser.json())
const USER_ROOT_DIR = '.data'
server.post("fs", (req, res) => {
let op = req.body.op;
switch(op){
case 'readFile':
fs.readFile(req.body.path, ()=>{})
case 'writeFile':
fs.writeFile(req.body.path, Buffer.from(req.body.data, "base64"), ()=>{})
}
})
server.listen(1337, ()=>{
PluginManager.instance.migratePlugins()
PluginManager.instance.setupPlugins()
setupMenu()
server.use("/v1", v1API)
// server.post("fs", (req, res) => {
// let op = req.body.op;
// switch(op){
// case 'readFile':
// fs.readFile(req.body.path, ()=>{})
// case 'writeFile':
// fs.writeFile(req.body.path, Buffer.from(req.body.data, "base64"), ()=>{})
// }
// })
server.listen(JAN_API_PORT, () => {
console.log(`JAN API listening at: http://localhost:${JAN_API_PORT}`);
})

View File

@ -1,24 +0,0 @@
import { Request } from "request";
/**
* Manages file downloads and network requests.
*/
export class DownloadManager {
public networkRequests: Record<string, any> = {};
public static instance: DownloadManager = new DownloadManager();
constructor() {
if (DownloadManager.instance) {
return DownloadManager.instance;
}
}
/**
* Sets a network request for a specific file.
* @param {string} fileName - The name of the file.
* @param {Request | undefined} request - The network request to set, or undefined to clear the request.
*/
setRequest(fileName: string, request: Request | undefined) {
this.networkRequests[fileName] = request;
}
}

View File

@ -1,33 +0,0 @@
import { dispose } from "../utils/disposable";
/**
* Manages imported modules.
*/
export class ModuleManager {
public requiredModules: Record<string, any> = {};
public static instance: ModuleManager = new ModuleManager();
constructor() {
if (ModuleManager.instance) {
return ModuleManager.instance;
}
}
/**
* Sets a module.
* @param {string} moduleName - The name of the module.
* @param {any | undefined} nodule - The module to set, or undefined to clear the module.
*/
setModule(moduleName: string, nodule: any | undefined) {
this.requiredModules[moduleName] = nodule;
}
/**
* Clears all imported modules.
*/
clearImportedModules() {
dispose(this.requiredModules);
this.requiredModules = {};
}
}

View File

@ -1,60 +0,0 @@
import { app } from "electron";
import { init } from "../core/plugin/index";
import { join } from "path";
import { rmdir } from "fs";
import Store from "electron-store";
/**
* Manages plugin installation and migration.
*/
export class PluginManager {
public static instance: PluginManager = new PluginManager();
constructor() {
if (PluginManager.instance) {
return PluginManager.instance;
}
}
/**
* Sets up the plugins by initializing the `plugins` module with the `confirmInstall` and `pluginsPath` options.
* The `confirmInstall` function always returns `true` to allow plugin installation.
* The `pluginsPath` option specifies the path to install plugins to.
*/
setupPlugins() {
init({
// Function to check from the main process that user wants to install a plugin
confirmInstall: async (_plugins: string[]) => {
return true;
},
// Path to install plugin to
pluginsPath: join(app.getPath("userData"), "plugins"),
});
}
/**
* Migrates the plugins by deleting the `plugins` directory in the user data path.
* If the `migrated_version` key in the `Store` object does not match the current app version,
* the function deletes the `plugins` directory and sets the `migrated_version` key to the current app version.
* @returns A Promise that resolves when the migration is complete.
*/
migratePlugins() {
return new Promise((resolve) => {
const store = new Store();
if (store.get("migrated_version") !== app.getVersion()) {
console.debug("start migration:", store.get("migrated_version"));
const userDataPath = app.getPath("userData");
const fullPath = join(userDataPath, "plugins");
rmdir(fullPath, { recursive: true }, function (err) {
if (err) console.error(err);
store.set("migrated_version", app.getVersion());
console.debug("migrate plugins done");
resolve(undefined);
});
} else {
resolve(undefined);
}
});
}
}

View File

@ -1,37 +0,0 @@
import { BrowserWindow } from "electron";
/**
* Manages the current window instance.
*/
export class WindowManager {
public static instance: WindowManager = new WindowManager();
public currentWindow?: BrowserWindow;
constructor() {
if (WindowManager.instance) {
return WindowManager.instance;
}
}
/**
* Creates a new window instance.
* @param {Electron.BrowserWindowConstructorOptions} options - The options to create the window with.
* @returns The created window instance.
*/
createWindow(options?: Electron.BrowserWindowConstructorOptions | undefined) {
this.currentWindow = new BrowserWindow({
width: 1200,
minWidth: 800,
height: 800,
show: false,
trafficLightPosition: {
x: 10,
y: 15,
},
titleBarStyle: "hidden",
vibrancy: "sidebar",
...options,
});
return this.currentWindow;
}
}

5
server/nodemon.json Normal file
View File

@ -0,0 +1,5 @@
{
"watch": ["main.ts", "v1"],
"ext": "ts, json",
"exec": "tsc && node ./build/main.js"
}

View File

@ -51,26 +51,15 @@
"scripts": {
"lint": "eslint . --ext \".js,.jsx,.ts,.tsx\"",
"test:e2e": "playwright test --workers=1",
"dev": "tsc -p . && electron .",
"build": "run-script-os",
"build:test": "run-script-os",
"build:test:darwin": "tsc -p . && electron-builder -p never -m --dir",
"build:test:win32": "tsc -p . && electron-builder -p never -w --dir",
"build:test:linux": "tsc -p . && electron-builder -p never -l --dir",
"build:darwin": "tsc -p . && electron-builder -p never -m",
"build:win32": "tsc -p . && electron-builder -p never -w",
"build:linux": "tsc -p . && electron-builder -p never --linux deb",
"build:publish": "run-script-os",
"build:publish:darwin": "tsc -p . && electron-builder -p onTagOrDraft -m --x64 --arm64",
"build:publish:win32": "tsc -p . && electron-builder -p onTagOrDraft -w",
"build:publish:linux": "tsc -p . && electron-builder -p onTagOrDraft -l deb"
"dev": "nodemon .",
"build": "tsc",
"build:test": "",
"build:publish": ""
},
"dependencies": {
"@npmcli/arborist": "^7.1.0",
"@types/request": "^2.48.12",
"@uiball/loaders": "^1.3.0",
"electron-store": "^8.1.0",
"electron-updater": "^6.1.4",
"express": "^4.18.2",
"pacote": "^17.0.4",
"request": "^2.88.2",
@ -78,7 +67,6 @@
"use-debounce": "^9.0.4"
},
"devDependencies": {
"@electron/notarize": "^2.1.0",
"@playwright/test": "^1.38.1",
"@types/body-parser": "^1.19.5",
"@types/express": "^4.17.21",
@ -86,10 +74,8 @@
"@types/pacote": "^11.1.7",
"@typescript-eslint/eslint-plugin": "^6.7.3",
"@typescript-eslint/parser": "^6.7.3",
"electron": "26.2.1",
"electron-builder": "^24.6.4",
"electron-playwright-helpers": "^1.6.0",
"eslint-plugin-react": "^7.33.2",
"nodemon": "^3.0.1",
"run-script-os": "^1.1.6"
},
"installConfig": {

View File

@ -15,6 +15,8 @@
"paths": { "*": ["node_modules/*"] },
"typeRoots": ["node_modules/@types"]
},
// "sourceMap": true,
"include": ["./**/*.ts"],
"exclude": ["core", "build", "dist", "tests", "node_modules"]
}

View File

@ -1,8 +0,0 @@
export function dispose(requiredModules: Record<string, any>) {
for (const key in requiredModules) {
const module = requiredModules[key];
if (typeof module["dispose"] === "function") {
module["dispose"]();
}
}
}

View File

@ -1,111 +0,0 @@
// @ts-nocheck
const { app, Menu, dialog } = require("electron");
const isMac = process.platform === "darwin";
const { autoUpdater } = require("electron-updater");
import { compareSemanticVersions } from "./versionDiff";
const template: (Electron.MenuItemConstructorOptions | Electron.MenuItem)[] = [
...(isMac
? [
{
label: app.name,
submenu: [
{ role: "about" },
{
label: "Check for Updates...",
click: () =>
autoUpdater.checkForUpdatesAndNotify().then((e) => {
if (
!e ||
compareSemanticVersions(
app.getVersion(),
e.updateInfo.version
) >= 0
)
dialog.showMessageBox({
message: `There are currently no updates available.`,
});
}),
},
{ type: "separator" },
{ role: "services" },
{ type: "separator" },
{ role: "hide" },
{ role: "hideOthers" },
{ role: "unhide" },
{ type: "separator" },
{ role: "quit" },
],
},
]
: []),
{
label: "Edit",
submenu: [
{ role: "undo" },
{ role: "redo" },
{ type: "separator" },
{ role: "cut" },
{ role: "copy" },
{ role: "paste" },
...(isMac
? [
{ role: "pasteAndMatchStyle" },
{ role: "delete" },
{ role: "selectAll" },
{ type: "separator" },
{
label: "Speech",
submenu: [{ role: "startSpeaking" }, { role: "stopSpeaking" }],
},
]
: [{ role: "delete" }, { type: "separator" }, { role: "selectAll" }]),
],
},
{
label: "View",
submenu: [
{ role: "reload" },
{ role: "forceReload" },
{ role: "toggleDevTools" },
{ type: "separator" },
{ role: "resetZoom" },
{ role: "zoomIn" },
{ role: "zoomOut" },
{ type: "separator" },
{ role: "togglefullscreen" },
],
},
{
label: "Window",
submenu: [
{ role: "minimize" },
{ role: "zoom" },
...(isMac
? [
{ type: "separator" },
{ role: "front" },
{ type: "separator" },
{ role: "window" },
]
: [{ role: "close" }]),
],
},
{
role: "help",
submenu: [
{
label: "Learn More",
click: async () => {
const { shell } = require("electron");
await shell.openExternal("https://jan.ai/");
},
},
],
},
];
export const setupMenu = () => {
const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);
};

View File

@ -1,21 +0,0 @@
export const compareSemanticVersions = (a: string, b: string) => {
// 1. Split the strings into their parts.
const a1 = a.split('.');
const b1 = b.split('.');
// 2. Contingency in case there's a 4th or 5th version
const len = Math.min(a1.length, b1.length);
// 3. Look through each version number and compare.
for (let i = 0; i < len; i++) {
const a2 = +a1[ i ] || 0;
const b2 = +b1[ i ] || 0;
if (a2 !== b2) {
return a2 > b2 ? 1 : -1;
}
}
// 4. We hit this if the all checked versions so far are equal
//
return b1.length - a1.length;
};

View File

@ -0,0 +1,5 @@
import { Request, Response } from 'express'
export default function route(req: Request, res: Response){
}

5
server/v1/chat/index.ts Normal file
View File

@ -0,0 +1,5 @@
import { Request, Response } from 'express'
export default function route(req: Request, res: Response){
}

20
server/v1/index.ts Normal file
View File

@ -0,0 +1,20 @@
import { Request, Response } from 'express'
import assistantsAPI from './assistants'
import chatCompletionAPI from './chat'
import modelsAPI from './models'
import threadsAPI from './threads'
export default function route(req: Request, res: Response){
console.log(req.path.split("/")[1])
switch (req.path.split("/")[1]){
case 'assistants':
assistantsAPI(req, res)
case 'chat':
chatCompletionAPI(req, res)
case 'models':
modelsAPI(req, res)
case 'threads':
threadsAPI(req, res)
}
}

View File

@ -0,0 +1,5 @@
import { Request, Response } from 'express'
export default function controller(req: Request, res: Response){
}

18
server/v1/models/index.ts Normal file
View File

@ -0,0 +1,18 @@
import { Request, Response } from 'express'
import downloadModelController from './downloadModel'
function getModelController(req: Request, res: Response){
}
export default function route(req: Request, res: Response){
switch(req.method){
case 'get':
getModelController(req, res)
break;
case 'post':
downloadModelController(req, res)
break;
}
}

View File

@ -0,0 +1,5 @@
import { Request, Response } from 'express'
export default function route(req: Request, res: Response){
}