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

feat: support plugin store and install plugin #337

Merged
merged 12 commits into from Mar 28, 2024
Merged
24 changes: 21 additions & 3 deletions libs/Universal-PicGo-Core/src/core/UniversalPicGo.ts
Expand Up @@ -35,7 +35,7 @@ import { ensureFileSync, ensureFolderSync, pathExistsSync } from "../utils/nodeU
import { I18nManager } from "../i18n"
import { browserPathJoin, getBrowserDirectoryPath } from "../utils/browserUtils"
import { isConfigKeyInBlackList, isInputConfigValid } from "../utils/common"
import { eventBus } from "../utils/eventBus"
import { picgoEventBus } from "../utils/picgoEventBus"
import { PicGoRequestWrapper } from "../lib/PicGoRequest"

/*
Expand All @@ -50,6 +50,7 @@ class UniversalPicGo extends EventEmitter implements IPicGo {
private db!: ConfigDb
private _pluginLoader!: PluginLoader
configPath: string
zhiNpmPath: string
baseDir!: string
pluginBaseDir!: string
helper!: IHelper
Expand All @@ -74,12 +75,13 @@ class UniversalPicGo extends EventEmitter implements IPicGo {
return this.requestWrapper.PicGoRequest.bind(this.requestWrapper)
}

constructor(configPath?: string, pluginBaseDir?: string, isDev?: boolean) {
constructor(configPath?: string, pluginBaseDir?: string, zhiNpmPath?: string, isDev?: boolean) {
super()
this.isDev = isDev ?? false
this.log = this.getLogger()
this.configPath = configPath ?? ""
this.pluginBaseDir = pluginBaseDir ?? ""
this.zhiNpmPath = zhiNpmPath ?? ""
this.output = []
this.input = []
this.helper = {
Expand All @@ -90,6 +92,7 @@ class UniversalPicGo extends EventEmitter implements IPicGo {
afterUploadPlugins: new LifecyclePlugins("afterUploadPlugins"),
}
this.initConfigPath()
this.initZhiNpmPath()
this.initConfig()
this.pluginHandler = new PluginHandler(this)
this.requestWrapper = new PicGoRequestWrapper(this)
Expand Down Expand Up @@ -152,7 +155,7 @@ class UniversalPicGo extends EventEmitter implements IPicGo {
delete config[name]
}
_.set(this._config, name, config[name])
eventBus.emit(IBusEvent.CONFIG_CHANGE, {
picgoEventBus.emit(IBusEvent.CONFIG_CHANGE, {
configName: name,
value: config[name],
})
Expand Down Expand Up @@ -269,6 +272,21 @@ class UniversalPicGo extends EventEmitter implements IPicGo {
this.log.info(`this.pluginBaseDir => ${this.pluginBaseDir}`)
}

private initZhiNpmPath(): void {
if (hasNodeEnv) {
const fs = win.fs
const path = win.require("path")

if (this.zhiNpmPath === "") {
this.zhiNpmPath = this.configPath
}
const dir = path.join(this.baseDir, "libs")
ensureFolderSync(fs, dir)
} else {
this.log.warn("zhi is not supported in browser")
}
}

private initConfig(): void {
this.db = new ConfigDb(this)
this._config = this.db.read(true) as IConfig
Expand Down
6 changes: 4 additions & 2 deletions libs/Universal-PicGo-Core/src/index.ts
Expand Up @@ -3,7 +3,7 @@ import { ExternalPicgo } from "./core/ExternalPicgo"
import ConfigDb from "./db/config"
import PluginLoaderDb from "./db/pluginLoder"
import ExternalPicgoConfigDb from "./db/externalPicGo"
import { eventBus } from "./utils/eventBus"
import { picgoEventBus } from "./utils/picgoEventBus"
import { currentWin, hasNodeEnv, parentWin, win } from "universal-picgo-store"
import { PicgoTypeEnum, IBusEvent } from "./utils/enums"
import {
Expand All @@ -16,10 +16,11 @@ import {
IUploaderConfigItem,
IUploaderConfigListItem,
IPluginConfig,
IPicGoPlugin,
} from "./types"
import { isFileOrBlob, calculateMD5 } from "./utils/common"

export { UniversalPicGo, ExternalPicgo, eventBus }
export { UniversalPicGo, ExternalPicgo, picgoEventBus }
export { ConfigDb, PluginLoaderDb, ExternalPicgoConfigDb }
export { PicgoTypeEnum, IBusEvent }
export { isFileOrBlob, calculateMD5 }
Expand All @@ -34,4 +35,5 @@ export {
type IUploaderConfigItem,
type IUploaderConfigListItem,
type IPluginConfig,
type IPicGoPlugin,
}
4 changes: 2 additions & 2 deletions libs/Universal-PicGo-Core/src/lib/PicGoRequest.ts
Expand Up @@ -11,7 +11,7 @@
import axios, { AxiosRequestConfig, AxiosResponse } from "axios"
import { IConfig, IConfigChangePayload, IFullResponse, IPicGo, IResponse, Undefinable } from "../types"
import { ILogger } from "zhi-lib-base"
import { eventBus } from "../utils/eventBus"
import { picgoEventBus } from "../utils/picgoEventBus"
import { IBusEvent } from "../utils/enums"
import { browserPathJoin } from "../utils/browserUtils"
import { hasNodeEnv } from "universal-picgo-store"
Expand Down Expand Up @@ -102,7 +102,7 @@ class PicGoRequestWrapper {
this.logger = ctx.getLogger("picgo-request")

this.init()
eventBus.on(IBusEvent.CONFIG_CHANGE, (data: IConfigChangePayload<string | IConfig["picBed"]>) => {
picgoEventBus.on(IBusEvent.CONFIG_CHANGE, (data: IConfigChangePayload<string | IConfig["picBed"]>) => {
switch (data.configName) {
case "picBed":
if ((data.value as IConfig["picBed"])?.proxy) {
Expand Down
188 changes: 183 additions & 5 deletions libs/Universal-PicGo-Core/src/lib/PluginHandler.ts
Expand Up @@ -7,32 +7,210 @@
* of this license document, but changing it is not allowed.
*/

import { IPicGo, IPluginHandler, IPluginHandlerOptions, IPluginHandlerResult, IProcessEnv } from "../types"
import {
IPicGo,
IPluginHandler,
IPluginHandlerOptions,
IPluginHandlerResult,
IPluginProcessResult,
IProcessEnv,
Undefinable,
} from "../types"
import { win } from "universal-picgo-store"
import { getNormalPluginName, getProcessPluginName } from "../utils/common"
import { ILocalesKey } from "../i18n/zh-CN"

export class PluginHandler implements IPluginHandler {
// Thanks to feflow -> https://github.com/feflow/feflow/blob/master/lib/internal/install/plugin.js
private readonly ctx: IPicGo

constructor(ctx: IPicGo) {
this.ctx = ctx
}

install(
async install(
plugins: string[],
options: IPluginHandlerOptions,
env: IProcessEnv | undefined
): Promise<IPluginHandlerResult<boolean>> {
throw new Error("PluginHandler.install not implemented")
// const result = await this.execCommand("-v")
// console.log("npm result =>", result)

const installedPlugins: string[] = []
const processPlugins = plugins
.map((item: string) => handlePluginNameProcess(this.ctx, item))
.filter((item) => {
// detect if has already installed
// or will cause error
if (this.ctx.pluginLoader.hasPlugin(item.pkgName)) {
installedPlugins.push(item.pkgName)
this.ctx.log.info(`PicGo has already installed ${item.pkgName}`)
return false
}
// if something wrong, filter it out
if (!item.success) {
return false
}
return true
})
const fullNameList = processPlugins.map((item: any) => item.fullName)
const pkgNameList = processPlugins.map((item: any) => item.pkgName)

if (fullNameList.length > 0) {
// install plugins must use fullNameList:
// 1. install remote pacage
// 2. install local pacage
const result = await this.execCommand("install", fullNameList, this.ctx.baseDir, options, env)
if (result.success) {
pkgNameList.forEach((pluginName: string) => {
this.ctx.pluginLoader.registerPlugin(pluginName)
})
this.ctx.log.info(this.ctx.i18n.translate<ILocalesKey>("PLUGIN_HANDLER_PLUGIN_INSTALL_SUCCESS"))
this.ctx.emit("installSuccess", {
title: this.ctx.i18n.translate<ILocalesKey>("PLUGIN_HANDLER_PLUGIN_INSTALL_SUCCESS"),
body: [...pkgNameList, ...installedPlugins],
})
const res: IPluginHandlerResult<true> = {
success: true,
body: [...pkgNameList, ...installedPlugins],
}
console.log("install plugin success =>", result)
return res
} else {
const err = this.ctx.i18n.translate<ILocalesKey>("PLUGIN_HANDLER_PLUGIN_INSTALL_FAILED_REASON", {
code: `-1`,
data: result.body,
})
this.ctx.log.error(err)
this.ctx.emit("installFailed", {
title: this.ctx.i18n.translate<ILocalesKey>("PLUGIN_HANDLER_PLUGIN_INSTALL_FAILED"),
body: err,
})
const res: IPluginHandlerResult<false> = {
success: false,
body: err,
}
return res
}
} else if (installedPlugins.length === 0) {
const err = this.ctx.i18n.translate<ILocalesKey>("PLUGIN_HANDLER_PLUGIN_UNINSTALL_FAILED_VALID")
this.ctx.log.error(err)
this.ctx.emit("installFailed", {
title: this.ctx.i18n.translate<ILocalesKey>("PLUGIN_HANDLER_PLUGIN_INSTALL_FAILED"),
body: err,
})
const res: IPluginHandlerResult<false> = {
success: false,
body: err,
}
return res
} else {
this.ctx.log.info(this.ctx.i18n.translate<ILocalesKey>("PLUGIN_HANDLER_PLUGIN_INSTALL_SUCCESS"))
this.ctx.emit("installSuccess", {
title: this.ctx.i18n.translate<ILocalesKey>("PLUGIN_HANDLER_PLUGIN_INSTALL_SUCCESS"),
body: [...pkgNameList, ...installedPlugins],
})
const res: IPluginHandlerResult<true> = {
success: true,
body: [...pkgNameList, ...installedPlugins],
}
return res
}
}

uninstall(plugins: string[]): Promise<IPluginHandlerResult<boolean>> {
async uninstall(plugins: string[]): Promise<IPluginHandlerResult<boolean>> {
throw new Error("PluginHandler.uninstall not implemented")
}

update(
async update(
plugins: string[],
options: IPluginHandlerOptions,
env: IProcessEnv | undefined
): Promise<IPluginHandlerResult<boolean>> {
throw new Error("PluginHandler.update not implemented")
}

// ===================================================================================================================
/**
* 执行 NPM 命令
*
* @param subCommand - 要执行的 NPM 命令
* @param modules - 模块数组
* @param cwd 当前路径
* @param options
* @param env 环境变量
* @returns 执行结果的 Promise
*/
private async execCommand(
subCommand: string,
modules: string[],
cwd?: string,
options: IPluginHandlerOptions = {},
env?: Record<string, any>
): Promise<any> {
try {
// 1、 require zhi-infra
const zhiInfraPath = `${this.ctx.baseDir}/libs/zhi-infra/index.cjs`
const setupjsPath = `${this.ctx.baseDir}/libs/setup`
await win.require(zhiInfraPath).default([setupjsPath, true])

// 2、await zhi.npm.checkAndInitNode()
await win.zhi.npm.checkAndInitNode()
console.log("node installed, start exec cmd...")

// exec cmd
// options first
const registry =
options.npmRegistry ||
this.ctx.getConfig<Undefinable<string>>("settings.npmRegistry") ||
"https://registry.npmmirror.com"
const proxy = options.npmProxy || this.ctx.getConfig<Undefinable<string>>("settings.npmProxy")
let args = modules.concat("--color=always").concat("--save")
if (registry) {
args = args.concat(`--registry=${registry}`)
}
if (proxy) {
args = args.concat(`--proxy=${proxy}`)
}

const res = await win.zhi.npm.localNodeExecCmd("npm", subCommand, undefined, args, cwd, env)
return {
success: true,
body: res,
}
} catch (e: any) {
return {
success: false,
body: "npm 命令执行异常 =>" + e.toString(),
}
}
}
}

/**
* transform the input plugin name or path string to valid result
* @param ctx
* @param nameOrPath
*/
const handlePluginNameProcess = (ctx: IPicGo, nameOrPath: string): IPluginProcessResult => {
const res = {
success: false,
fullName: "",
pkgName: "",
}
const result = getProcessPluginName(nameOrPath, ctx.log)
if (!result) {
return res
}
// first get result then do this process
// or some error will log twice
const pkgName = getNormalPluginName(result, ctx.log)
if (!pkgName) {
return res
}
return {
success: true,
fullName: result,
pkgName,
}
}
5 changes: 2 additions & 3 deletions libs/Universal-PicGo-Core/src/lib/PluginLoader.ts
Expand Up @@ -65,7 +65,7 @@ export class PluginLoader implements IPluginLoader {
const path = this.resolvePlugin(this.ctx, name)
return false
})
this.logger.warn("load is not supported in browser")
this.logger.warn("plugin load is not supported in browser")
return false
}
}
Expand Down Expand Up @@ -139,8 +139,7 @@ export class PluginLoader implements IPluginLoader {

const path = win.require("path")
const pluginDir = path.join(this.ctx.pluginBaseDir, "node_modules/")
// eslint-disable-next-line @typescript-eslint/no-var-requires
const plugin = require(pluginDir + name)(this.ctx)
const plugin = win.require(pluginDir + name)(this.ctx)
this.pluginMap.set(name, plugin)
return plugin
}
Expand Down
14 changes: 13 additions & 1 deletion libs/Universal-PicGo-Core/src/types/index.d.ts
Expand Up @@ -629,4 +629,16 @@ interface IUploaderListItemMetaInfo {
_configName: string
_updatedAt: number
_createdAt: number
}
}

export interface IPluginProcessResult {
success: boolean
/**
* the package.json's name filed
*/
pkgName: string
/**
* the plugin name or the fs absolute path
*/
fullName: string
}