feat: explore plugins from the npm repository and install them remotely (#399)
* feat: explore plugins from the npm repository and install them remotely * refactor: clean out redundant codes * chore: only show update button on different version
This commit is contained in:
parent
1f40c26cc5
commit
c5925b6a79
@ -1,11 +1,12 @@
|
|||||||
import { app, BrowserWindow, ipcMain, dialog, shell } from "electron";
|
import { app, BrowserWindow, ipcMain, dialog, shell } from "electron";
|
||||||
import { readdirSync } from "fs";
|
import { readdirSync, writeFileSync } from "fs";
|
||||||
import { resolve, join, extname } from "path";
|
import { resolve, join, extname } from "path";
|
||||||
import { rmdir, unlink, createWriteStream } from "fs";
|
import { rmdir, unlink, createWriteStream } from "fs";
|
||||||
import { init } from "./core/plugin-manager/pluginMgr";
|
import { init } from "./core/plugin-manager/pluginMgr";
|
||||||
import { setupMenu } from "./utils/menu";
|
import { setupMenu } from "./utils/menu";
|
||||||
import { dispose } from "./utils/disposable";
|
import { dispose } from "./utils/disposable";
|
||||||
|
|
||||||
|
const pacote = require("pacote");
|
||||||
const request = require("request");
|
const request = require("request");
|
||||||
const progress = require("request-progress");
|
const progress = require("request-progress");
|
||||||
const { autoUpdater } = require("electron-updater");
|
const { autoUpdater } = require("electron-updater");
|
||||||
@ -225,6 +226,24 @@ function handleIPCs() {
|
|||||||
})
|
})
|
||||||
.pipe(createWriteStream(destination));
|
.pipe(createWriteStream(destination));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 pacote
|
||||||
|
.manifest(pluginName)
|
||||||
|
.then(async (manifest: any) => {
|
||||||
|
await pacote.tarball(manifest._resolved).then((data: Buffer) => {
|
||||||
|
writeFileSync(destination, data);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(() => destination);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function migratePlugins() {
|
function migratePlugins() {
|
||||||
|
|||||||
@ -37,7 +37,9 @@
|
|||||||
"icon": "icons/icon.png"
|
"icon": "icons/icon.png"
|
||||||
},
|
},
|
||||||
"linux": {
|
"linux": {
|
||||||
"target": ["deb"],
|
"target": [
|
||||||
|
"deb"
|
||||||
|
],
|
||||||
"category": "Utility",
|
"category": "Utility",
|
||||||
"icon": "icons/"
|
"icon": "icons/"
|
||||||
},
|
},
|
||||||
|
|||||||
@ -23,24 +23,19 @@ contextBridge.exposeInMainWorld("electronAPI", {
|
|||||||
|
|
||||||
deleteFile: (filePath: string) => ipcRenderer.invoke("deleteFile", filePath),
|
deleteFile: (filePath: string) => ipcRenderer.invoke("deleteFile", filePath),
|
||||||
|
|
||||||
downloadFile: (url: string, path: string) =>
|
installRemotePlugin: (pluginName: string) => ipcRenderer.invoke("installRemotePlugin", pluginName),
|
||||||
ipcRenderer.invoke("downloadFile", url, path),
|
|
||||||
|
|
||||||
onFileDownloadUpdate: (callback: any) =>
|
downloadFile: (url: string, path: string) => ipcRenderer.invoke("downloadFile", url, path),
|
||||||
ipcRenderer.on("FILE_DOWNLOAD_UPDATE", callback),
|
|
||||||
|
|
||||||
onFileDownloadError: (callback: any) =>
|
onFileDownloadUpdate: (callback: any) => ipcRenderer.on("FILE_DOWNLOAD_UPDATE", callback),
|
||||||
ipcRenderer.on("FILE_DOWNLOAD_ERROR", callback),
|
|
||||||
|
|
||||||
onFileDownloadSuccess: (callback: any) =>
|
onFileDownloadError: (callback: any) => ipcRenderer.on("FILE_DOWNLOAD_ERROR", callback),
|
||||||
ipcRenderer.on("FILE_DOWNLOAD_COMPLETE", callback),
|
|
||||||
|
|
||||||
onAppUpdateDownloadUpdate: (callback: any) =>
|
onFileDownloadSuccess: (callback: any) => ipcRenderer.on("FILE_DOWNLOAD_COMPLETE", callback),
|
||||||
ipcRenderer.on("APP_UPDATE_PROGRESS", callback),
|
|
||||||
|
|
||||||
onAppUpdateDownloadError: (callback: any) =>
|
onAppUpdateDownloadUpdate: (callback: any) => ipcRenderer.on("APP_UPDATE_PROGRESS", callback),
|
||||||
ipcRenderer.on("APP_UPDATE_ERROR", callback),
|
|
||||||
|
|
||||||
onAppUpdateDownloadSuccess: (callback: any) =>
|
onAppUpdateDownloadError: (callback: any) => ipcRenderer.on("APP_UPDATE_ERROR", callback),
|
||||||
ipcRenderer.on("APP_UPDATE_COMPLETE", callback),
|
|
||||||
|
onAppUpdateDownloadSuccess: (callback: any) => ipcRenderer.on("APP_UPDATE_COMPLETE", callback),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,17 +1,13 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import {
|
import { plugins, extensionPoints } from "@/../../electron/core/plugin-manager/execution/index";
|
||||||
setup,
|
|
||||||
plugins,
|
|
||||||
extensionPoints,
|
|
||||||
activationPoints,
|
|
||||||
} from "@/../../electron/core/plugin-manager/execution/index";
|
|
||||||
import { ChartPieIcon, CommandLineIcon, PlayIcon } from "@heroicons/react/24/outline";
|
import { ChartPieIcon, CommandLineIcon, PlayIcon } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
|
import { MagnifyingGlassIcon } from "@heroicons/react/20/solid";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { PluginService, preferences } from "@janhq/core";
|
import { PluginService, preferences } from "@janhq/core";
|
||||||
import { execute } from "../../../electron/core/plugin-manager/execution/extension-manager";
|
import { execute } from "../../../electron/core/plugin-manager/execution/extension-manager";
|
||||||
|
import LoadingIndicator from "./LoadingIndicator";
|
||||||
|
|
||||||
export const Preferences = () => {
|
export const Preferences = () => {
|
||||||
const [search, setSearch] = useState<string>("");
|
const [search, setSearch] = useState<string>("");
|
||||||
@ -20,62 +16,68 @@ export const Preferences = () => {
|
|||||||
const [preferenceValues, setPreferenceValues] = useState<any[]>([]);
|
const [preferenceValues, setPreferenceValues] = useState<any[]>([]);
|
||||||
const [isTestAvailable, setIsTestAvailable] = useState(false);
|
const [isTestAvailable, setIsTestAvailable] = useState(false);
|
||||||
const [fileName, setFileName] = useState("");
|
const [fileName, setFileName] = useState("");
|
||||||
|
const [pluginCatalog, setPluginCatalog] = useState<any[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||||
const experimentRef = useRef(null);
|
const experimentRef = useRef(null);
|
||||||
const preferenceRef = useRef(null);
|
const preferenceRef = useRef(null);
|
||||||
|
|
||||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
/**
|
||||||
const file = event.target.files?.[0];
|
* Loads the plugin catalog module from a CDN and sets it as the plugin catalog state.
|
||||||
if (file) {
|
* The `webpackIgnore` comment is used to prevent Webpack from bundling the module.
|
||||||
setFileName(file.name);
|
*/
|
||||||
} else {
|
|
||||||
setFileName("");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function setupPE() {
|
// @ts-ignore
|
||||||
// Enable activation point management
|
import(/* webpackIgnore: true */ PLUGIN_CATALOGS).then((module) => {
|
||||||
setup({
|
console.log(module);
|
||||||
//@ts-ignore
|
setPluginCatalog(module.default);
|
||||||
importer: (plugin) =>
|
});
|
||||||
import(/* webpackIgnore: true */ plugin).catch((err) => {
|
|
||||||
console.log(err);
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Register all active plugins with their activation points
|
|
||||||
await plugins.registerActive();
|
|
||||||
}
|
|
||||||
|
|
||||||
const activePlugins = async () => {
|
|
||||||
const plgs = await plugins.getActive();
|
|
||||||
setActivePlugins(plgs);
|
|
||||||
// Activate alls
|
|
||||||
setTimeout(async () => {
|
|
||||||
await activationPoints.trigger("init");
|
|
||||||
if (extensionPoints.get("experimentComponent")) {
|
|
||||||
const components = await Promise.all(extensionPoints.execute("experimentComponent"));
|
|
||||||
if (components.length > 0) {
|
|
||||||
setIsTestAvailable(true);
|
|
||||||
}
|
|
||||||
components.forEach((e) => {
|
|
||||||
if (experimentRef.current) {
|
|
||||||
// @ts-ignore
|
|
||||||
experimentRef.current.appendChild(e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extensionPoints.get("PluginPreferences")) {
|
|
||||||
const data = await Promise.all(extensionPoints.execute("PluginPreferences"));
|
|
||||||
setPreferenceItems(Array.isArray(data) ? data : []);
|
|
||||||
}
|
|
||||||
}, 500);
|
|
||||||
};
|
|
||||||
setupPE().then(() => activePlugins());
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Install a new plugin on clicking the install button
|
/**
|
||||||
|
* Fetches the active plugins and their preferences from the `plugins` and `preferences` modules.
|
||||||
|
* If the `experimentComponent` extension point is available, it executes the extension point and
|
||||||
|
* appends the returned components to the `experimentRef` element.
|
||||||
|
* If the `PluginPreferences` extension point is available, it executes the extension point and
|
||||||
|
* fetches the preferences for each plugin using the `preferences.get` function.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const getActivePlugins = async () => {
|
||||||
|
const plgs = await plugins.getActive();
|
||||||
|
setActivePlugins(plgs);
|
||||||
|
|
||||||
|
if (extensionPoints.get("experimentComponent")) {
|
||||||
|
const components = await Promise.all(extensionPoints.execute("experimentComponent"));
|
||||||
|
if (components.length > 0) {
|
||||||
|
setIsTestAvailable(true);
|
||||||
|
}
|
||||||
|
components.forEach((e) => {
|
||||||
|
if (experimentRef.current) {
|
||||||
|
// @ts-ignore
|
||||||
|
experimentRef.current.appendChild(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (extensionPoints.get("PluginPreferences")) {
|
||||||
|
const data = await Promise.all(extensionPoints.execute("PluginPreferences"));
|
||||||
|
setPreferenceItems(Array.isArray(data) ? data : []);
|
||||||
|
Promise.all(
|
||||||
|
(Array.isArray(data) ? data : []).map((e) =>
|
||||||
|
preferences.get(e.pluginName, e.preferenceKey).then((k) => ({ key: e.preferenceKey, value: k }))
|
||||||
|
)
|
||||||
|
).then((data) => {
|
||||||
|
setPreferenceValues(data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
getActivePlugins();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs a plugin by calling the `plugins.install` function with the plugin file path.
|
||||||
|
* If the installation is successful, the application is relaunched using the `coreAPI.relaunch` function.
|
||||||
|
* @param e - The event object.
|
||||||
|
*/
|
||||||
const install = async (e: any) => {
|
const install = async (e: any) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
@ -84,27 +86,51 @@ export const Preferences = () => {
|
|||||||
// Send the filename of the to be installed plugin
|
// Send the filename of the to be installed plugin
|
||||||
// to the main process for installation
|
// to the main process for installation
|
||||||
const installed = await plugins.install([pluginFile]);
|
const installed = await plugins.install([pluginFile]);
|
||||||
if (installed) window.electronAPI.relaunch();
|
if (installed) window.coreAPI?.relaunch();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Uninstall a plugin on clicking uninstall
|
/**
|
||||||
|
* Uninstalls a plugin by calling the `plugins.uninstall` function with the plugin name.
|
||||||
|
* If the uninstallation is successful, the application is relaunched using the `coreAPI.relaunch` function.
|
||||||
|
* @param name - The name of the plugin to uninstall.
|
||||||
|
*/
|
||||||
const uninstall = async (name: string) => {
|
const uninstall = async (name: string) => {
|
||||||
// Send the filename of the to be uninstalled plugin
|
// Send the filename of the to be uninstalled plugin
|
||||||
// to the main process for removal
|
// to the main process for removal
|
||||||
const res = await plugins.uninstall([name]);
|
const res = await plugins.uninstall([name]);
|
||||||
if (res) window.electronAPI.relaunch();
|
if (res) window.coreAPI?.relaunch();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update all plugins on clicking update plugins
|
/**
|
||||||
|
* Updates a plugin by calling the `window.pluggableElectronIpc.update` function with the plugin name.
|
||||||
|
* If the update is successful, the application is relaunched using the `window.coreAPI.relaunch` function.
|
||||||
|
* TODO: should update using window.coreAPI rather than pluggableElectronIpc (Plugin Manager Facades)
|
||||||
|
* @param plugin - The name of the plugin to update.
|
||||||
|
*/
|
||||||
const update = async (plugin: string) => {
|
const update = async (plugin: string) => {
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
await window.pluggableElectronIpc.update([plugin], true);
|
await window.pluggableElectronIpc.update([plugin], true);
|
||||||
window.electronAPI.reloadPlugins();
|
window.coreAPI?.relaunch();
|
||||||
}
|
}
|
||||||
// plugins.update(active.map((plg) => plg.name));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads a remote plugin tarball and installs it using the `plugins.install` function.
|
||||||
|
* If the installation is successful, the application is relaunched using the `coreAPI.relaunch` function.
|
||||||
|
* @param pluginName - The name of the remote plugin to download and install.
|
||||||
|
*/
|
||||||
|
const downloadTarball = async (pluginName: string) => {
|
||||||
|
setIsLoading(true);
|
||||||
|
const pluginPath = await window.coreAPI?.installRemotePlugin(pluginName);
|
||||||
|
const installed = await plugins.install([pluginPath]);
|
||||||
|
setIsLoading(false);
|
||||||
|
if (installed) window.coreAPI.relaunch();
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Notifies plugins of a preference update by executing the `PluginService.OnPreferencesUpdate` event.
|
||||||
|
* If a timeout is already set, it is cleared before setting a new timeout to execute the event.
|
||||||
|
*/
|
||||||
let timeout: any | undefined = undefined;
|
let timeout: any | undefined = undefined;
|
||||||
function notifyPreferenceUpdate() {
|
function notifyPreferenceUpdate() {
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
@ -113,17 +139,19 @@ export const Preferences = () => {
|
|||||||
timeout = setTimeout(() => execute(PluginService.OnPreferencesUpdate), 100);
|
timeout = setTimeout(() => execute(PluginService.OnPreferencesUpdate), 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
/**
|
||||||
if (preferenceItems) {
|
* Handles the change event of the plugin file input element by setting the file name state.
|
||||||
Promise.all(
|
* Its to be used to display the plugin file name of the selected file.
|
||||||
preferenceItems.map((e) =>
|
* @param event - The change event object.
|
||||||
preferences.get(e.pluginName, e.preferenceKey).then((k) => ({ key: e.preferenceKey, value: k }))
|
*/
|
||||||
)
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
).then((data) => {
|
const file = event.target.files?.[0];
|
||||||
setPreferenceValues(data);
|
if (file) {
|
||||||
});
|
setFileName(file.name);
|
||||||
|
} else {
|
||||||
|
setFileName("");
|
||||||
}
|
}
|
||||||
}, [preferenceItems]);
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-screen overflow-scroll">
|
<div className="w-full h-screen overflow-scroll">
|
||||||
@ -200,7 +228,7 @@ export const Preferences = () => {
|
|||||||
"bg-blue-500 hover:bg-blue-300 rounded-md px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
"bg-blue-500 hover:bg-blue-300 rounded-md px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600"
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.electronAPI.reloadPlugins();
|
window.coreAPI?.reloadPlugins();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reload Plugins
|
Reload Plugins
|
||||||
@ -227,9 +255,7 @@ export const Preferences = () => {
|
|||||||
<img className="h-14 w-14 rounded-md" src={e.icon ?? "icons/app_icon.svg"} alt="" />
|
<img className="h-14 w-14 rounded-md" src={e.icon ?? "icons/app_icon.svg"} alt="" />
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className="text-xl font-bold tracking-tight text-gray-900 dark:text-white">
|
<p className="text-xl font-bold tracking-tight text-gray-900 dark:text-white">{e.name}</p>
|
||||||
{e.name}
|
|
||||||
</p>
|
|
||||||
<p className="font-normal text-gray-700 dark:text-gray-400">Version: {e.version}</p>
|
<p className="font-normal text-gray-700 dark:text-gray-400">Version: {e.version}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -260,6 +286,53 @@ export const Preferences = () => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-row items-center my-4">
|
||||||
|
<CommandLineIcon width={30} />
|
||||||
|
Explore Plugins
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-2 items-stretch gap-4">
|
||||||
|
{pluginCatalog
|
||||||
|
.filter((e: any) => search.trim() === "" || e.name.toLowerCase().includes(search.toLowerCase()))
|
||||||
|
.map((e: any) => (
|
||||||
|
<div
|
||||||
|
key={e.name}
|
||||||
|
data-testid="plugin-item"
|
||||||
|
className="flex flex-col h-full p-6 bg-white border border-gray-200 rounded-sm dark:border-gray-300"
|
||||||
|
>
|
||||||
|
<div className="flex flex-row space-x-2 items-center">
|
||||||
|
<span className="relative inline-block mt-1">
|
||||||
|
<img className="h-14 w-14 rounded-md" src={e.icon ?? "icons/app_icon.svg"} alt="" />
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<p className="text-xl font-bold tracking-tight text-gray-900 dark:text-white">{e.name}</p>
|
||||||
|
<p className="font-normal text-gray-700 dark:text-gray-400">Version: {e.version}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="flex-1 mt-2 text-sm font-normal text-gray-500 dark:text-gray-400 w-full">
|
||||||
|
{e.description ?? "Jan's Plugin"}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="flex flex-row space-x-5">
|
||||||
|
{e.version !== activePlugins.filter((p) => p.name === e.name)[0]?.version && (
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
onClick={() => downloadTarball(e.name)}
|
||||||
|
className={classNames(
|
||||||
|
"mt-5 rounded-md px-3.5 py-2.5 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-600",
|
||||||
|
activePlugins.some((p) => p.name === e.name)
|
||||||
|
? "bg-blue-500 hover:bg-blue-600"
|
||||||
|
: "bg-red-500 hover:bg-red-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{activePlugins.some((p) => p.name === e.name) ? "Update" : "Install"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
{activePlugins.length > 0 && isTestAvailable && (
|
{activePlugins.length > 0 && isTestAvailable && (
|
||||||
<div className="flex flex-row items-center my-4">
|
<div className="flex flex-row items-center my-4">
|
||||||
<PlayIcon width={30} />
|
<PlayIcon width={30} />
|
||||||
@ -296,6 +369,12 @@ export const Preferences = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
{isLoading && (
|
||||||
|
<div className="z-50 absolute inset-0 bg-gray-900/90 flex justify-center items-center text-white">
|
||||||
|
<LoadingIndicator />
|
||||||
|
Installing...
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
|
const webpack = require("webpack");
|
||||||
|
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
output: "export",
|
output: "export",
|
||||||
assetPrefix: ".",
|
assetPrefix: ".",
|
||||||
@ -18,6 +20,12 @@ const nextConfig = {
|
|||||||
// do some stuff here
|
// do some stuff here
|
||||||
config.optimization.minimize = false;
|
config.optimization.minimize = false;
|
||||||
config.optimization.minimizer = [];
|
config.optimization.minimizer = [];
|
||||||
|
config.plugins = [
|
||||||
|
...config.plugins,
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
PLUGIN_CATALOGS: JSON.stringify("https://cdn.jsdelivr.net/npm/@janhq/plugin-catalog@latest/dist/index.js"),
|
||||||
|
}),
|
||||||
|
];
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
1
web/types/index.d.ts
vendored
1
web/types/index.d.ts
vendored
@ -1,5 +1,6 @@
|
|||||||
export {};
|
export {};
|
||||||
|
|
||||||
|
declare const PLUGIN_CATALOGS: string[];
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
electronAPI?: any | undefined;
|
electronAPI?: any | undefined;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user