diff --git a/libs/zhi-blog-api/CHANGELOG.md b/libs/zhi-blog-api/CHANGELOG.md index 2aa928f6..1ea6cbcd 100644 --- a/libs/zhi-blog-api/CHANGELOG.md +++ b/libs/zhi-blog-api/CHANGELOG.md @@ -1,5 +1,127 @@ # zhi-blog-api +## 1.45.0 + +### Minor Changes + +- feat: not force lowercase + +## 1.44.4 + +### Patch Changes + +- fix: remove same item + +## 1.44.3 + +### Patch Changes + +- Updated dependencies + - zhi-common@1.24.0 + +## 1.44.2 + +### Patch Changes + +- fix: add search + +## 1.44.1 + +### Patch Changes + +- fix: add search option + +## 1.44.0 + +### Minor Changes + +- feat: add search for getCategories + +## 1.43.0 + +### Minor Changes + +- feat: add mdFilename + +## 1.42.0 + +### Minor Changes + +- feat: add usePathCategory + +## 1.41.0 + +### Minor Changes + +- feat: add useMdFilename + +## 1.40.0 + +### Minor Changes + +- feat: add mdFilename + +## 1.39.0 + +### Minor Changes + +- feat: export TagInfo + +## 1.38.0 + +### Minor Changes + +- feat: add getTags method + +## 1.37.1 + +### Patch Changes + +- feat: add tagSlugEnabled + +## 1.37.0 + +### Minor Changes + +- feat: add tags_slugs field + +## 1.36.3 + +### Patch Changes + +- feat: add filename for uploadFile + +## 1.36.2 + +### Patch Changes + +- fix: deduplicate duplicate null values + +## 1.36.1 + +### Patch Changes + +- fix; change empty categories + +## 1.36.0 + +### Minor Changes + +- feat: YAML serialization requires merge labels and classifications + +## 1.35.0 + +### Minor Changes + +- feat: Extract fromYaml and toYamlObj + +## 1.34.14 + +### Patch Changes + +- Updated dependencies + - zhi-common@1.23.7 + ## 1.34.13 ### Patch Changes diff --git a/libs/zhi-blog-api/package.json b/libs/zhi-blog-api/package.json index 53eebb08..46a4b17b 100644 --- a/libs/zhi-blog-api/package.json +++ b/libs/zhi-blog-api/package.json @@ -1,6 +1,6 @@ { "name": "zhi-blog-api", - "version": "1.34.13", + "version": "1.45.0", "type": "module", "description": "a common blog interface", "main": "./dist/index.js", diff --git a/libs/zhi-blog-api/src/index.ts b/libs/zhi-blog-api/src/index.ts index 8e319308..cccec8e4 100644 --- a/libs/zhi-blog-api/src/index.ts +++ b/libs/zhi-blog-api/src/index.ts @@ -21,13 +21,16 @@ import CategoryTypeEnum from "./lib/enums/categoryTypeEnum" import YamlFormatObj from "./lib/models/yamlFormatObj" import YamlConvertAdaptor from "./lib/yamlConvertAdaptor" import PreferenceConfig from "./lib/PreferenceConfig" +import PostUtil from "./lib/PostUtil" +import TagInfo from "./lib/models/tagInfo" export { BlogApi, BlogAdaptor } export { WebApi, WebAdaptor } export { BlogConfig, BlogPlaceholder, PasswordType, PageType, PostStatusEnum } export { WebConfig, WebPlaceholder } -export { Post, UserBlog, SiteConfig, CategoryInfo, MediaObject, Attachment } +export { Post, UserBlog, SiteConfig, CategoryInfo, TagInfo, MediaObject, Attachment } export { BlogConstants, BlogTypeEnum, PageTypeEnum, CategoryTypeEnum } export { YamlFormatObj, YamlConvertAdaptor } +export { PostUtil } export { PreferenceConfig } export { type ElectronCookie } diff --git a/libs/zhi-blog-api/src/lib/IBlogApi.ts b/libs/zhi-blog-api/src/lib/IBlogApi.ts index 8d2d457b..b4776f3d 100644 --- a/libs/zhi-blog-api/src/lib/IBlogApi.ts +++ b/libs/zhi-blog-api/src/lib/IBlogApi.ts @@ -29,6 +29,7 @@ import MediaObject from "./models/mediaObject" import CategoryInfo from "./models/categoryInfo" import Attachment from "./models/attachmentInfo" import YamlConvertAdaptor from "./yamlConvertAdaptor" +import TagInfo from "./models/tagInfo" /** * 通用博客接口 @@ -42,10 +43,12 @@ import YamlConvertAdaptor from "./yamlConvertAdaptor" interface IBlogApi { /** * 博客配置列表 + * + * @param keyword - 搜索关键字,部分平台不支持 * @see {@link https://codex.wordpress.org/XML-RPC_MetaWeblog_API#metaWeblog.getUsersBlogs getUsersBlogs} * @returns {Promise>} */ - getUsersBlogs(): Promise> + getUsersBlogs(keyword?: string): Promise> /** * 最新文章数目 @@ -161,10 +164,16 @@ interface IBlogApi { /** * 获取分类列表 * + * @param keyword - 搜索关键字,部分平台不支持 * @see {@link https://codex.wordpress.org/XML-RPC_MetaWeblog_API#metaWeblog.getCategories getCategories} * @returns {Promise} */ - getCategories(): Promise + getCategories(keyword?: string): Promise + + /** + * 获取标签列表 + */ + getTags(): Promise /** * 获取文件树列表 diff --git a/libs/zhi-blog-api/src/lib/IWebApi.ts b/libs/zhi-blog-api/src/lib/IWebApi.ts index 114b7389..f34f4a51 100644 --- a/libs/zhi-blog-api/src/lib/IWebApi.ts +++ b/libs/zhi-blog-api/src/lib/IWebApi.ts @@ -80,9 +80,10 @@ interface IWebApi extends IBlogApi { * 上传图片:调用平台 API 上传图片 * * @param file 图片文件 + * @param filename 文件名,可选 * @returns Promise 上传后的图片地址 */ - uploadFile(file: File): Promise + uploadFile(file: File, filename?: string): Promise /** * 更新文章:调用平台 API 更新文章(发布工具内部通过该接口替换文章内图片地址) diff --git a/libs/zhi-blog-api/src/lib/PostUtil.spec.ts b/libs/zhi-blog-api/src/lib/PostUtil.spec.ts new file mode 100644 index 00000000..76732858 --- /dev/null +++ b/libs/zhi-blog-api/src/lib/PostUtil.spec.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2023, Terwer . All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Terwer designates this + * particular file as subject to the "Classpath" exception as provided + * by Terwer in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Terwer, Shenzhen, Guangdong, China, youweics@163.com + * or visit www.terwer.space if you need additional information or have any + * questions. + */ + +import { describe, it } from "vitest" + +describe("test PostUtil", () => { + it("test fromYaml", () => { + + }) +}) diff --git a/libs/zhi-blog-api/src/lib/PostUtil.ts b/libs/zhi-blog-api/src/lib/PostUtil.ts new file mode 100644 index 00000000..45f6276c --- /dev/null +++ b/libs/zhi-blog-api/src/lib/PostUtil.ts @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2023, Terwer . All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Terwer designates this + * particular file as subject to the "Classpath" exception as provided + * by Terwer in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Terwer, Shenzhen, Guangdong, China, youweics@163.com + * or visit www.terwer.space if you need additional information or have any + * questions. + */ + +import Post from "./models/post" +import { DateUtil } from "zhi-common" + +/** + * 文章处理工具类 + * + * @since 1.11.2 + */ +class PostUtil { + /** + * 将当前对象的数据转换为适用于 YAML 的对象 + * + * @returns {Object} 表示数据的适用于 YAML 的对象 + */ + public static toYamlObj(post: Post): Record { + const yamlObj: Record = {} + + post.dateCreated && (yamlObj.created = DateUtil.formatIsoToZh(post.dateCreated.toISOString(), true)) + post.dateUpdated && (yamlObj.updated = DateUtil.formatIsoToZh(post.dateUpdated.toISOString(), true)) + post.title && (yamlObj.title = post.title) + post.wp_slug && (yamlObj.slug = post.wp_slug) + post.permalink && (yamlObj.permalink = post.permalink) + post.shortDesc && (yamlObj.desc = post.shortDesc) + post.mt_keywords && (yamlObj.tags = post.mt_keywords?.split(",")) + post.categories && (yamlObj.categories = post.categories) + + return yamlObj + } + + /** + * 使用来自适用于 YAML 的对象的数据填充当前对象的属性 + * + * @param post - 文章对象 + * @param {Object} yamlObj - 包含要填充对象属性的数据的适用于 YAML 的对象 + */ + public static fromYaml(post: Post, yamlObj: Record): void { + post.dateCreated = yamlObj?.created ? DateUtil.convertStringToDate(yamlObj.created) : post.dateCreated + post.dateUpdated = + (yamlObj?.updated ? DateUtil.convertStringToDate(yamlObj.updated) : post.dateUpdated) ?? new Date() + post.title = yamlObj?.title ?? post.title + post.wp_slug = yamlObj?.slug ?? post.wp_slug + post.permalink = yamlObj?.permalink ?? post.permalink + post.shortDesc = yamlObj?.desc ?? post.shortDesc + + // // 标签合并 + // if (yamlObj?.tags && yamlObj.tags.length > 0) { + // const existingTags = post.mt_keywords.split(",") + // const uniqueKeywords = [...new Set([...existingTags, ...yamlObj.tags])].filter((tag) => tag.trim() !== "") + // post.mt_keywords = uniqueKeywords.length > 0 ? uniqueKeywords.join(",") : "" + // } + // + // // 分类合并 + // if (yamlObj?.categories && yamlObj.categories.length > 0) { + // const combinedCategories = [...new Set([...post.categories, ...yamlObj.categories])].filter( + // (cate) => cate.trim() !== "" + // ) + // post.categories = combinedCategories.length > 0 ? combinedCategories : [] + // } + + // 标签合并 + if (yamlObj?.tags && yamlObj.tags.length > 0) { + const existingTags = post.mt_keywords.split(",") + const uniqueKeywords = [ + ...new Set([...existingTags.map((tag) => tag.trim()), ...yamlObj.tags.map((tag) => tag.trim())]), + ].filter((tag) => tag.trim() !== "") + + post.mt_keywords = uniqueKeywords.length > 0 ? uniqueKeywords.join(",") : "" + } + + // 分类合并 + if (yamlObj?.categories && yamlObj.categories.length > 0) { + const combinedCategories = [ + ...new Set([...post.categories.map((cate) => cate.trim()), ...yamlObj.categories.map((cate) => cate.trim())]), + ].filter((cate) => cate.trim() !== "") + + post.categories = combinedCategories.length > 0 ? combinedCategories : [] + } + } +} + +export default PostUtil diff --git a/libs/zhi-blog-api/src/lib/blogAdaptor.ts b/libs/zhi-blog-api/src/lib/blogAdaptor.ts index 8ef8d4ee..85720f07 100644 --- a/libs/zhi-blog-api/src/lib/blogAdaptor.ts +++ b/libs/zhi-blog-api/src/lib/blogAdaptor.ts @@ -32,6 +32,7 @@ import { simpleLogger } from "zhi-lib-base" import Attachment from "./models/attachmentInfo" import BlogApi from "./blogApi" import YamlConvertAdaptor from "./yamlConvertAdaptor" +import TagInfo from "./models/tagInfo" /** * 博客API @@ -63,8 +64,8 @@ class BlogAdaptor implements IBlogApi { /** * 博客配置列表 */ - public async getUsersBlogs(): Promise> { - return await this.apiAdaptor.getUsersBlogs() + public async getUsersBlogs(keyword?: string): Promise> { + return await this.apiAdaptor.getUsersBlogs(keyword) } /** @@ -147,11 +148,18 @@ class BlogAdaptor implements IBlogApi { return await this.apiAdaptor.deletePost(postid) } + /** + * 获取标签列表 + */ + public async getTags(): Promise { + return await this.apiAdaptor.getTags() + } + /** * 获取分类列表 */ - public async getCategories(): Promise { - return await this.apiAdaptor.getCategories() + public async getCategories(keyword?: string): Promise { + return await this.apiAdaptor.getCategories(keyword) } /** diff --git a/libs/zhi-blog-api/src/lib/blogApi.ts b/libs/zhi-blog-api/src/lib/blogApi.ts index 44085820..306e0f60 100644 --- a/libs/zhi-blog-api/src/lib/blogApi.ts +++ b/libs/zhi-blog-api/src/lib/blogApi.ts @@ -31,12 +31,13 @@ import MediaObject from "./models/mediaObject" import { NotImplementedException } from "zhi-lib-base" import Attachment from "./models/attachmentInfo" import YamlConvertAdaptor from "./yamlConvertAdaptor" +import TagInfo from "./models/tagInfo" /** * 博客基类 */ class BlogApi implements IBlogApi { - public async getUsersBlogs(): Promise> { + public async getUsersBlogs(keyword?: string): Promise> { throw new NotImplementedException("You must implement getUsersBlogs in sub class") } @@ -68,10 +69,14 @@ class BlogApi implements IBlogApi { throw new NotImplementedException("You must implement deletePost in sub class") } - public async getCategories(): Promise { + public async getCategories(keyword?: string): Promise { throw new NotImplementedException("You must implement getCategories in sub class") } + getTags(): Promise { + throw new NotImplementedException("You must implement getTags in sub class") + } + public async getCategoryTreeNodes(docPath: string): Promise { throw new NotImplementedException("You must implement getCategoryTreeNodes in sub class") } diff --git a/libs/zhi-blog-api/src/lib/blogConfig.ts b/libs/zhi-blog-api/src/lib/blogConfig.ts index 2a7fd4a8..85cb36b5 100644 --- a/libs/zhi-blog-api/src/lib/blogConfig.ts +++ b/libs/zhi-blog-api/src/lib/blogConfig.ts @@ -229,6 +229,26 @@ abstract class BlogConfig { */ public yamlLinkEnabled: boolean + /** + * 是否运行标签别名 + */ + public tagSlugEnabled: boolean + + /** + * 是否使用标题作为MD 文件名 + */ + useMdFilename?: boolean + + /** + * 是否使用路径作为分类 + */ + usePathCategory?: boolean + + /** + * 是否允许分类搜索 + */ + public cateSearchEnabled?: boolean + protected constructor() { this.home = "" this.apiUrl = "" @@ -257,6 +277,10 @@ abstract class BlogConfig { this.allowCateChange = false this.categoryType = CategoryTypeEnum.CategoryType_None this.yamlLinkEnabled = true + this.tagSlugEnabled = false + this.useMdFilename = false + this.usePathCategory = false + this.cateSearchEnabled = false } } diff --git a/libs/zhi-blog-api/src/lib/models/post.ts b/libs/zhi-blog-api/src/lib/models/post.ts index 5a1fc14d..cb1a2880 100644 --- a/libs/zhi-blog-api/src/lib/models/post.ts +++ b/libs/zhi-blog-api/src/lib/models/post.ts @@ -42,11 +42,6 @@ class Post { */ title: string - /** - * 逗号分隔的标签 - */ - mt_keywords: string - /** * 链接 */ @@ -67,6 +62,11 @@ class Post { */ yaml: string + /** + * MD 文件名,不包括 .md + */ + mdFilename?: string + /** * HTML正文 */ @@ -107,10 +107,20 @@ class Post { */ dateUpdated: Date + /** + * 逗号分隔的标签 + */ + mt_keywords: string + + /** + * 标签别名,大部分平台不需要 + */ + tags_slugs?: string + /** * 分类 */ - categories: Array + categories: string[] /** * 分类别名,大部分平台不需要 @@ -145,15 +155,17 @@ class Post { constructor() { this.postid = "" this.title = "" - this.mt_keywords = "" this.permalink = "" this.yaml = "---\n---" + this.mdFilename = "test" this.html = "" this.markdown = "" this.editorDom = "" this.description = "" this.wp_slug = "" this.dateCreated = new Date() + this.mt_keywords = "" + this.tags_slugs = "" this.categories = [] this.cate_slugs = [] this.isPublished = true @@ -161,47 +173,6 @@ class Post { this.wp_password = "" this.attrs = "{}" } - - /** - * 将当前对象的数据转换为适用于 YAML 的对象 - * - * @returns {Object} 表示数据的适用于 YAML 的对象 - */ - public toYamlObj(): Record { - const yamlObj: Record = {} - - this.dateCreated && (yamlObj.created = DateUtil.formatIsoToZh(this.dateCreated.toISOString(), true)) - this.dateUpdated && (yamlObj.updated = DateUtil.formatIsoToZh(this.dateUpdated.toISOString(), true)) - this.title && (yamlObj.title = this.title) - this.wp_slug && (yamlObj.slug = this.wp_slug) - this.permalink && (yamlObj.permalink = this.permalink) - this.shortDesc && (yamlObj.desc = this.shortDesc) - this.mt_keywords && (yamlObj.tags = this.mt_keywords?.split(",")) - this.categories && (yamlObj.categories = this.categories) - - return yamlObj - } - - /** - * 使用来自适用于 YAML 的对象的数据填充当前对象的属性 - * - * @param {Object} yamlObj - 包含要填充对象属性的数据的适用于 YAML 的对象 - */ - public fromYaml(yamlObj: Record): void { - this.dateCreated = yamlObj?.created ? DateUtil.convertStringToDate(yamlObj.created) : this.dateCreated - this.dateUpdated = yamlObj?.updated ? DateUtil.convertStringToDate(yamlObj.updated) : this.dateUpdated - this.title = yamlObj?.title ?? this.title - this.wp_slug = yamlObj?.slug ?? this.wp_slug - this.permalink = yamlObj?.permalink ?? this.permalink - this.shortDesc = yamlObj?.desc ?? this.shortDesc - // 修复历史遗留问题 - if (typeof yamlObj?.tags === "string" && yamlObj?.tags.indexOf(",") > -1) { - this.mt_keywords = yamlObj?.tag - } else { - this.mt_keywords = yamlObj?.tags ? yamlObj?.tags.join(",") : this.mt_keywords - } - this.categories = yamlObj?.categories ?? this.categories - } } export default Post diff --git a/libs/zhi-blog-api/src/lib/models/tagInfo.ts b/libs/zhi-blog-api/src/lib/models/tagInfo.ts new file mode 100644 index 00000000..c27eae86 --- /dev/null +++ b/libs/zhi-blog-api/src/lib/models/tagInfo.ts @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2023, Terwer . All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. Terwer designates this + * particular file as subject to the "Classpath" exception as provided + * by Terwer in the LICENSE file that accompanied this code. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Terwer, Shenzhen, Guangdong, China, youweics@163.com + * or visit www.terwer.space if you need additional information or have any + * questions. + */ + +class TagInfo { + /** + * 标签ID + */ + tagId: string + + /** + * 父标签ID + */ + parentId: string + + /** + * 标签名称 + */ + description: string + + /** + * 标签描述 + */ + tagDescription: string + + /** + * 标签英文名 + */ + tagName: string + + constructor() { + this.tagId = "-1" + this.parentId = "0" + this.description = "标签1" + this.tagDescription = "这是测试标签1" + this.tagName = "cate1" + } +} + +export default TagInfo diff --git a/libs/zhi-blog-api/src/lib/webAdaptor.ts b/libs/zhi-blog-api/src/lib/webAdaptor.ts index 24640bb3..bb0e480b 100644 --- a/libs/zhi-blog-api/src/lib/webAdaptor.ts +++ b/libs/zhi-blog-api/src/lib/webAdaptor.ts @@ -81,8 +81,8 @@ class WebAdaptor extends BlogAdaptor { return await this.webAdaptor.addPost(post) } - public async uploadFile(file: File): Promise { - return await this.webAdaptor.uploadFile(file) + public async uploadFile(file: File, filename?: string): Promise { + return await this.webAdaptor.uploadFile(file, filename) } public async editPost(postid: string, post: Post, publish?: boolean): Promise { diff --git a/libs/zhi-blog-api/src/lib/webApi.ts b/libs/zhi-blog-api/src/lib/webApi.ts index 58920b5c..b24f2399 100644 --- a/libs/zhi-blog-api/src/lib/webApi.ts +++ b/libs/zhi-blog-api/src/lib/webApi.ts @@ -54,7 +54,7 @@ class WebApi extends BlogApi implements IWebApi { throw new NotImplementedException("You must implement addPost in sub class") } - public async uploadFile(file: File): Promise { + public async uploadFile(file: File, filename?: string): Promise { throw new NotImplementedException("You must implement uploadFile in sub class") } diff --git a/libs/zhi-common/CHANGELOG.md b/libs/zhi-common/CHANGELOG.md index df9fc19e..eea07fd5 100644 --- a/libs/zhi-common/CHANGELOG.md +++ b/libs/zhi-common/CHANGELOG.md @@ -1,5 +1,17 @@ # zhi-common +## 1.24.0 + +### Minor Changes + +- fix: change yaml regex + +## 1.23.7 + +### Patch Changes + +- feat: Optimize YAML parsing + ## 1.23.6 ### Patch Changes diff --git a/libs/zhi-common/package.json b/libs/zhi-common/package.json index e83b3c0e..e3ff35a3 100644 --- a/libs/zhi-common/package.json +++ b/libs/zhi-common/package.json @@ -1,6 +1,6 @@ { "name": "zhi-common", - "version": "1.23.6", + "version": "1.24.0", "type": "module", "description": "a collection of util tools", "main": "./dist/index.js", diff --git a/libs/zhi-common/src/lib/yamlUtil.spec.ts b/libs/zhi-common/src/lib/yamlUtil.spec.ts index 08320eec..dcf0a671 100644 --- a/libs/zhi-common/src/lib/yamlUtil.spec.ts +++ b/libs/zhi-common/src/lib/yamlUtil.spec.ts @@ -59,4 +59,44 @@ toc: true const yaml = YamlUtil.obj2Yaml(obj) console.log(yaml) }) + + it("test extractMarkdown", () => { + const md = `--- +title: 'WordPress未开启xmlrpc的时候给出友好提示 · Issue #643 · terwersiyuan-plugin-publisher' +date: '2023-08-31 11:09:31' +updated: '2023-08-31 11:12:18' +permalink: /post/wordpress-does-not-open-xmlrpc-to-give-friendly-prompts-nbtkf.html +comments: true +toc: true +--- +# WordPress未开启xmlrpc的时候给出友好提示 · Issue #643 · terwersiyuan-plugin-publisher + +--- + +* [https://github.com/terwer/siyuan-plugin-publisher/issues/643 - GitHub](https://github.com/terwer/siyuan-plugin-publisher/issues/643) +* Slash commands +* 2023-08-31 11:09:31 + +--- + +**Slash commands** + +Beta + +[ Give feedback ](https://github.com/feedback/slash-commands) + +**Slash commands** + +Beta + +[ Give feedback ](https://github.com/feedback/slash-commands) + +#### An unexpected error has occurred + +** ** Attach files by dragging & dropping, selecting or pasting them. ** ** ** ** Uploading your files… ** ** ** We don’t support that file type. ** **Try again** with a GIF, JPEG, JPG, MOV, MP4, PNG, SVG, WEBM, CSV, DOCX, FODG, FODP, FODS, FODT, GZ, LOG, MD, ODF, ODG, ODP, ODS, ODT, PATCH, PDF, PPTX, TGZ, TXT, XLS, XLSX or ZIP. ** ** ** Attaching documents requires write permission to this repository. ** **Try again** with a GIF, JPEG, JPG, MOV, MP4, PNG, SVG, WEBM, CSV, DOCX, FODG, FODP, FODS, FODT, GZ, LOG, MD, ODF, ODG, ODP, ODS, ODT, PATCH, PDF, PPTX, TGZ, TXT, XLS, XLSX or ZIP. ** ** ** We don’t support that file type. ** **Try again** with a GIF, JPEG, JPG, MOV, MP4, PNG, SVG, WEBM, CSV, DOCX, FODG, FODP, FODS, FODT, GZ, LOG, MD, ODF, ODG, ODP, ODS, ODT, PATCH, PDF, PPTX, TGZ, TXT, XLS, XLSX or ZIP. ** ** ** ** ** This file is empty. ** **Try again** with a file that’s not empty. ** ** ** This file is hidden. ** **Try again** with another file. ** ** ** Something went really wrong, and we can’t process that file. ** **Try again.** ** ** [ ](https://docs.github.com/github/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax) +` + const result = YamlUtil.extractMarkdown(md) + const result2 = YamlUtil.extractMarkdown(result) + console.log(result2) + }) }) diff --git a/libs/zhi-common/src/lib/yamlUtil.ts b/libs/zhi-common/src/lib/yamlUtil.ts index 88724caa..dbd9422d 100644 --- a/libs/zhi-common/src/lib/yamlUtil.ts +++ b/libs/zhi-common/src/lib/yamlUtil.ts @@ -35,6 +35,7 @@ import { simpleLogger } from "zhi-lib-base" */ class YamlUtil { private static logger = simpleLogger("yaml-util") + private static YAML_REGEX = /^-{3}\n([\s\S]*?\n)-{3}/ /** * yaml转对象 @@ -43,7 +44,11 @@ class YamlUtil { */ public static yaml2Obj(content: string): any { const frontMatter = this.extractFrontmatter(content) - return jsYaml.load(frontMatter, {}) + let ret = jsYaml.load(frontMatter, {}) + if (!ret) { + ret = {} + } + return ret } /** @@ -56,11 +61,8 @@ class YamlUtil { public static async yaml2ObjAsync(content: string): Promise { // 提取 YAML const frontMatter = this.extractFrontmatter(content, true) - // 去掉分隔符 - const regex = /^---\n([\s\S]*)\n---$/ - const match = frontMatter.match(regex) - + const match = frontMatter.match(this.YAML_REGEX) if (match) { const rawContent = match[1] try { @@ -94,9 +96,7 @@ class YamlUtil { * @param addSign - 是否包含符号 */ public static extractFrontmatter(content: string, addSign?: boolean): any { - const regex = /^---\n([\s\S]*?\n)---/ - const match = content.match(regex) - + const match = content.match(this.YAML_REGEX) if (match) { let frontMatter = match[1].trim() if (addSign) { @@ -114,11 +114,9 @@ class YamlUtil { * @param content - 包含正文和前置数据的字符串 */ public static extractMarkdown(content: string): any { - const regex = /^---\n([\s\S]*?\n)---/ - let markdown = content - if (regex.test(content)) { - markdown = content.replace(regex, "") + if (this.YAML_REGEX.test(content)) { + markdown = content.replace(this.YAML_REGEX, "") this.logger.info("发现原有的YAML,已移除") } diff --git a/libs/zhi-fetch-middleware/CHANGELOG.md b/libs/zhi-fetch-middleware/CHANGELOG.md index 58a60ab8..92337731 100644 --- a/libs/zhi-fetch-middleware/CHANGELOG.md +++ b/libs/zhi-fetch-middleware/CHANGELOG.md @@ -1,5 +1,19 @@ # zhi-fetch-middleware +## 0.6.5 + +### Patch Changes + +- Updated dependencies + - zhi-common@1.24.0 + +## 0.6.4 + +### Patch Changes + +- Updated dependencies + - zhi-common@1.23.7 + ## 0.6.3 ### Patch Changes diff --git a/libs/zhi-fetch-middleware/package.json b/libs/zhi-fetch-middleware/package.json index 94cbf325..46ad96d3 100644 --- a/libs/zhi-fetch-middleware/package.json +++ b/libs/zhi-fetch-middleware/package.json @@ -1,6 +1,6 @@ { "name": "zhi-fetch-middleware", - "version": "0.6.3", + "version": "0.6.5", "type": "module", "description": "an intermediate tier prepared for fetch requests", "main": "./dist/index.js", diff --git a/libs/zhi-github-middleware/CHANGELOG.md b/libs/zhi-github-middleware/CHANGELOG.md index 624df7fe..ce536ea6 100644 --- a/libs/zhi-github-middleware/CHANGELOG.md +++ b/libs/zhi-github-middleware/CHANGELOG.md @@ -1,5 +1,19 @@ # zhi-github-middleware +## 0.4.3 + +### Patch Changes + +- Updated dependencies + - zhi-common@1.24.0 + +## 0.4.2 + +### Patch Changes + +- Updated dependencies + - zhi-common@1.23.7 + ## 0.4.1 ### Patch Changes diff --git a/libs/zhi-github-middleware/package.json b/libs/zhi-github-middleware/package.json index a4147125..83c8e0a0 100644 --- a/libs/zhi-github-middleware/package.json +++ b/libs/zhi-github-middleware/package.json @@ -1,6 +1,6 @@ { "name": "zhi-github-middleware", - "version": "0.4.1", + "version": "0.4.3", "type": "module", "description": "a middleware for github api", "main": "./dist/index.js", diff --git a/libs/zhi-gitlab-middleware/CHANGELOG.md b/libs/zhi-gitlab-middleware/CHANGELOG.md index 864575c4..8838a1fb 100644 --- a/libs/zhi-gitlab-middleware/CHANGELOG.md +++ b/libs/zhi-gitlab-middleware/CHANGELOG.md @@ -1,5 +1,17 @@ # zhi-gitlab-middleware +## 0.6.5 + +### Patch Changes + +- zhi-fetch-middleware@0.6.5 + +## 0.6.4 + +### Patch Changes + +- zhi-fetch-middleware@0.6.4 + ## 0.6.3 ### Patch Changes diff --git a/libs/zhi-gitlab-middleware/package.json b/libs/zhi-gitlab-middleware/package.json index 428972a2..837a6549 100644 --- a/libs/zhi-gitlab-middleware/package.json +++ b/libs/zhi-gitlab-middleware/package.json @@ -1,6 +1,6 @@ { "name": "zhi-gitlab-middleware", - "version": "0.6.3", + "version": "0.6.5", "type": "module", "description": "a middleware for gitlab api ", "main": "./dist/index.js", diff --git a/libs/zhi-picgo-core/.eslintignore b/libs/zhi-picgo-core/.eslintignore new file mode 100644 index 00000000..f06235c4 --- /dev/null +++ b/libs/zhi-picgo-core/.eslintignore @@ -0,0 +1,2 @@ +node_modules +dist diff --git a/libs/zhi-picgo-core/.eslintrc.cjs b/libs/zhi-picgo-core/.eslintrc.cjs new file mode 100644 index 00000000..a1993b7b --- /dev/null +++ b/libs/zhi-picgo-core/.eslintrc.cjs @@ -0,0 +1,4 @@ +module.exports = { + root: true, + extends: ["./node_modules/@terwer/eslint-config-custom/typescript/index.cjs"], +} diff --git a/libs/zhi-picgo-core/CHANGELOG.md b/libs/zhi-picgo-core/CHANGELOG.md new file mode 100644 index 00000000..33e2220d --- /dev/null +++ b/libs/zhi-picgo-core/CHANGELOG.md @@ -0,0 +1,401 @@ +# zhi-picgo-core + +## 1.7.0 + +### Minor Changes + +- feat: fit latest siyuan note + +## 1.6.1 + +### Patch Changes + +- chore: format code + +# :tada: 1.5.0 (2022-11-13) + +# :tada: 1.5.0-alpha.17 (2022-11-13) + +### :sparkles: Features + +- update picgo.use ([e19bb6e](https://github.com/PicGo/PicGo-Core/commit/e19bb6e)) + +### :bug: Bug Fixes + +- some case will cause proxy not work ([6272303](https://github.com/PicGo/PicGo-Core/commit/6272303)) + +# :tada: 1.5.0-alpha.16 (2022-11-09) + +### :bug: Bug Fixes + +- upyun url-options is not required ([9736a11](https://github.com/PicGo/PicGo-Core/commit/9736a11)) + +# :tada: 1.5.0-alpha.15 (2022-10-24) + +### :bug: Bug Fixes + +- tencent cos url encode bug ([eafb70f](https://github.com/PicGo/PicGo-Core/commit/eafb70f)) + +# :tada: 1.5.0-alpha.14 (2022-10-24) + +### :bug: Bug Fixes + +- url encode bug ([4c70e9b](https://github.com/PicGo/PicGo-Core/commit/4c70e9b)) + +# :tada: 1.5.0-alpha.13 (2022-10-19) + +### :bug: Bug Fixes + +- type error ([b934e8a](https://github.com/PicGo/PicGo-Core/commit/b934e8a)) + +### :package: Chore + +- create oldRequest types ([749359a](https://github.com/PicGo/PicGo-Core/commit/749359a)) + +# :tada: 1.5.0-alpha.12 (2022-10-14) + +### :bug: Bug Fixes + +- types bug ([d77e6f3](https://github.com/PicGo/PicGo-Core/commit/d77e6f3)) + +# :tada: 1.5.0-alpha.11 (2022-10-13) + +# :tada: 1.5.0-alpha.10 (2022-09-25) + +### :sparkles: Features + +- add zh-TW ([#135](https://github.com/PicGo/PicGo-Core/issues/135)) ([d111d6a](https://github.com/PicGo/PicGo-Core/commit/d111d6a)) +- update linux.sh ([#134](https://github.com/PicGo/PicGo-Core/issues/134)) ([2910c20](https://github.com/PicGo/PicGo-Core/commit/2910c20)) + +### :bug: Bug Fixes + +- some texts in zh-TW ([#136](https://github.com/PicGo/PicGo-Core/issues/136)) ([907e6c9](https://github.com/PicGo/PicGo-Core/commit/907e6c9)) +- url image download bug & tencent cos url encode bug ([53d54f8](https://github.com/PicGo/PicGo-Core/commit/53d54f8)) + +# :tada: 1.5.0-alpha.9 (2022-09-03) + +### :sparkles: Features + +- finish request -> axios ([b89cf1e](https://github.com/PicGo/PicGo-Core/commit/b89cf1e)) + +### :bug: Bug Fixes + +- qiniu error msg can't show ([0e4661b](https://github.com/PicGo/PicGo-Core/commit/0e4661b)) +- sm.ms backupDomain message text ([45424d1](https://github.com/PicGo/PicGo-Core/commit/45424d1)) +- when request-options resolveWithFullResponse is false bug ([eb8217a](https://github.com/PicGo/PicGo-Core/commit/eb8217a)) + +# :tada: 1.5.0-alpha.8 (2022-08-27) + +### :sparkles: Features + +- add backupDomain for sm.ms ([c6d54f1](https://github.com/PicGo/PicGo-Core/commit/c6d54f1)) +- add debug logger type ([4342268](https://github.com/PicGo/PicGo-Core/commit/4342268)) +- add picgo.use for easily using plugin ([c0107f1](https://github.com/PicGo/PicGo-Core/commit/c0107f1)) + +### :bug: Bug Fixes + +- sometime tencent-cloud error message is empty ([6355e1b](https://github.com/PicGo/PicGo-Core/commit/6355e1b)) + +# :tada: 1.5.0-alpha.7 (2022-08-20) + +### :sparkles: Features + +- finish i18n text ([11b3197](https://github.com/PicGo/PicGo-Core/commit/11b3197)) + +### :bug: Bug Fixes + +- aliyun content-type -> Content-Type ([a649fcc](https://github.com/PicGo/PicGo-Core/commit/a649fcc)) + +# :tada: 1.5.0-alpha.6 (2022-08-17) + +### :bug: Bug Fixes + +- tencent cos upload error ([61df53a](https://github.com/PicGo/PicGo-Core/commit/61df53a)) + +# :tada: 1.5.0-alpha.5 (2022-07-31) + +### :sparkles: Features + +- add log file size limit ([158be01](https://github.com/PicGo/PicGo-Core/commit/158be01)) +- change inner db to @picgo/store ([0e90af3](https://github.com/PicGo/PicGo-Core/commit/0e90af3)) + +### :bug: Bug Fixes + +- build error ([674a6b5](https://github.com/PicGo/PicGo-Core/commit/674a6b5)) + +# :tada: 1.5.0-alpha.4 (2022-05-26) + +### :sparkles: Features + +- add userAgent for tencent cloud COS ([acac59a](https://github.com/PicGo/PicGo-Core/commit/acac59a)) + +# :tada: 1.5.0-alpha.3 (2022-04-04) + +### :bug: Bug Fixes + +- picgo-gui clipboard image uploading error ([1302f76](https://github.com/PicGo/PicGo-Core/commit/1302f76)) + +# :tada: 1.5.0-alpha.2 (2022-04-03) + +### :sparkles: Features + +- add wayland support for linux ([#119](https://github.com/PicGo/PicGo-Core/issues/119)) ([28905f2](https://github.com/PicGo/PicGo-Core/commit/28905f2)) + +### :bug: Bug Fixes + +- qiniu && upyun errors ([587dd3f](https://github.com/PicGo/PicGo-Core/commit/587dd3f)) + +# :tada: 1.5.0-alpha.1 (2022-03-08) + +### :sparkles: Features + +- add options for tencent cos ([1fccdcc](https://github.com/PicGo/PicGo-Core/commit/1fccdcc)), closes [#117](https://github.com/PicGo/PicGo-Core/issues/117) +- **i18n:** add i18n for picgo ([4b93a76](https://github.com/PicGo/PicGo-Core/commit/4b93a76)) + +### :bug: Bug Fixes + +- build error in windows ([5616fb9](https://github.com/PicGo/PicGo-Core/commit/5616fb9)) + +### :package: Chore + +- add alpha branch for alpha version ([6882022](https://github.com/PicGo/PicGo-Core/commit/6882022)), closes [#106](https://github.com/PicGo/PicGo-Core/issues/106) + +# :tada: 1.5.0-alpha.0 (2021-10-26) + +### :package: Chore + +- **build:** migrate to esbuild and change export assignment to esm export ([#102](https://github.com/PicGo/PicGo-Core/issues/102)) ([2a6cd18](https://github.com/PicGo/PicGo-Core/commit/2a6cd18)) + +## :tada: 1.4.26 (2021-08-23) + +### :bug: Bug Fixes + +- engine bug in package.json ([1c65144](https://github.com/PicGo/PicGo-Core/commit/1c65144)) + +## :tada: 1.4.25 (2021-08-21) + +### :bug: Bug Fixes + +- handle clipboard file path error ([ff4ec86](https://github.com/PicGo/PicGo-Core/commit/ff4ec86)), closes [#97](https://github.com/PicGo/PicGo-Core/issues/97) +- **error:** throw error when transform failed ([#96](https://github.com/PicGo/PicGo-Core/issues/96)) ([57fce75](https://github.com/PicGo/PicGo-Core/commit/57fce75)) +- clipboard path contains space ([#95](https://github.com/PicGo/PicGo-Core/issues/95)) ([d2b73c1](https://github.com/PicGo/PicGo-Core/commit/d2b73c1)) + +## :tada: 1.4.24 (2021-08-01) + +### :sparkles: Features + +- uploaded now can be modified since picgo will not use this value ([b6a8b58](https://github.com/PicGo/PicGo-Core/commit/b6a8b58)) + +## :tada: 1.4.23 (2021-07-27) + +### :bug: Bug Fixes + +- cases when clipboard of wsl contain image file ([#91](https://github.com/PicGo/PicGo-Core/issues/91)) ([adfc55e](https://github.com/PicGo/PicGo-Core/commit/adfc55e)) + +### :package: Chore + +- add github actions for publishing ([caae80e](https://github.com/PicGo/PicGo-Core/commit/caae80e)) + +## :tada: 1.4.22 (2021-07-27) + +### :sparkles: Features + +- **smms:** smms now supports image without token ([#89](https://github.com/PicGo/PicGo-Core/issues/89)) ([456b81c](https://github.com/PicGo/PicGo-Core/commit/456b81c)) +- add support for clipboard in wsl ([#87](https://github.com/PicGo/PicGo-Core/issues/87)) ([3e230de](https://github.com/PicGo/PicGo-Core/commit/3e230de)) + +## :tada: 1.4.21 (2021-05-09) + +### :bug: Bug Fixes + +- output empty after uploading when using isolate context ([79c228b](https://github.com/PicGo/PicGo-Core/commit/79c228b)) + +## :tada: 1.4.20 (2021-05-09) + +### :sparkles: Features + +- add createContext for each upload process ([ecde023](https://github.com/PicGo/PicGo-Core/commit/ecde023)) + +### :package: Chore + +- add debug launch.json ([9950259](https://github.com/PicGo/PicGo-Core/commit/9950259)) + +## :tada: 1.4.19 (2021-04-04) + +### :sparkles: Features + +- add current uploader && transformer log ([67b2bb1](https://github.com/PicGo/PicGo-Core/commit/67b2bb1)) +- limit some of config's capabilities ([f901505](https://github.com/PicGo/PicGo-Core/commit/f901505)) + +## :tada: 1.4.18 (2021-03-06) + +### :bug: Bug Fixes + +- unregister plugin delete pluginMap ([85228d8](https://github.com/PicGo/PicGo-Core/commit/85228d8)) + +## :tada: 1.4.17 (2021-02-09) + +### :bug: Bug Fixes + +- **type:** type error in index.d.ts ([f617658](https://github.com/PicGo/PicGo-Core/commit/f617658)), closes [#69](https://github.com/PicGo/PicGo-Core/issues/69) + +## :tada: 1.4.16 (2021-02-08) + +### :sparkles: Features + +- add proxy & registry options for pluginHandler ([b10b963](https://github.com/PicGo/PicGo-Core/commit/b10b963)) +- dynamic proxy getter with ctx.Request.request ([687805f](https://github.com/PicGo/PicGo-Core/commit/687805f)), closes [#64](https://github.com/PicGo/PicGo-Core/issues/64) +- supporting install specific version of plugin ([35e15b0](https://github.com/PicGo/PicGo-Core/commit/35e15b0)) + +## :tada: 1.4.15 (2021-01-24) + +### :sparkles: Features + +- add local plugin install/uninstall/update support & imporve plugin name handler ([f8ec464](https://github.com/PicGo/PicGo-Core/commit/f8ec464)) + +## :tada: 1.4.14 (2020-12-19) + +### :bug: Bug Fixes + +- types error ([303a4ec](https://github.com/PicGo/PicGo-Core/commit/303a4ec)) + +## :tada: 1.4.13 (2020-12-19) + +### :sparkles: Features + +- new addPlugin api for node projects ([5a18432](https://github.com/PicGo/PicGo-Core/commit/5a18432)) + +### :bug: Bug Fixes + +- **type:** some type error ([233a6ca](https://github.com/PicGo/PicGo-Core/commit/233a6ca)) +- pluginLoader can't get the full plugin list ([83535b9](https://github.com/PicGo/PicGo-Core/commit/83535b9)), closes [#60](https://github.com/PicGo/PicGo-Core/issues/60) + +## :tada: 1.4.12 (2020-11-04) + +### :bug: Bug Fixes + +- let tcyun error info more detail ([ddf645f](https://github.com/PicGo/PicGo-Core/commit/ddf645f)) +- not encode before uploading an image with url ([c0aee32](https://github.com/PicGo/PicGo-Core/commit/c0aee32)) +- qiniu error handler ([de94212](https://github.com/PicGo/PicGo-Core/commit/de94212)) + +## :tada: 1.4.11 (2020-07-12) + +### :bug: Bug Fixes + +- initailize db function error ([df7d526](https://github.com/PicGo/PicGo-Core/commit/df7d526)) + +## :tada: 1.4.10 (2020-06-28) + +### :bug: Bug Fixes + +- url image hash bug ([e405221](https://github.com/PicGo/PicGo-Core/commit/e405221)) + +## :tada: 1.4.9 (2020-06-27) + +### :sparkles: Features + +- add plugin running && error logs ([6adc070](https://github.com/PicGo/PicGo-Core/commit/6adc070)) +- **transformer:** add fallback to support more image formats such as HEIC ([0f5d2a9](https://github.com/PicGo/PicGo-Core/commit/0f5d2a9)), closes [#13](https://github.com/PicGo/PicGo-Core/issues/13) + +### :bug: Bug Fixes + +- multiline logs format ([444a42f](https://github.com/PicGo/PicGo-Core/commit/444a42f)) +- the issue of lost logs ([daa7508](https://github.com/PicGo/PicGo-Core/commit/daa7508)) +- the order of the uploaded list may not be the same as the order entered ([2bf1ed9](https://github.com/PicGo/PicGo-Core/commit/2bf1ed9)), closes [#40](https://github.com/PicGo/PicGo-Core/issues/40) +- unregisterPlugin's bug ([966bfd8](https://github.com/PicGo/PicGo-Core/commit/966bfd8)) + +### :package: Chore + +- add vscode workspace settings & migrate tslint to eslint ([50a4842](https://github.com/PicGo/PicGo-Core/commit/50a4842)) + +## :tada: 1.4.8 (2020-04-04) + +### :bug: Bug Fixes + +- encode url before finishing ([7a6b39c](https://github.com/PicGo/PicGo-Core/commit/7a6b39c)) +- return true if decodeURI throw error to avoid crash ([d09d77a](https://github.com/PicGo/PicGo-Core/commit/d09d77a)) +- win10 cmd crash bug when "picgo upload" ([#35](https://github.com/PicGo/PicGo-Core/issues/35)) ([deec252](https://github.com/PicGo/PicGo-Core/commit/deec252)) + +## :tada: 1.4.7 (2020-03-07) + +### :sparkles: Features + +- add smms-v2 support ([7e10655](https://github.com/PicGo/PicGo-Core/commit/7e10655)) +- remove weibo support ([96b2b3a](https://github.com/PicGo/PicGo-Core/commit/96b2b3a)) + +### :pencil: Documentation + +- update README ([aff6326](https://github.com/PicGo/PicGo-Core/commit/aff6326)) + +## :tada: 1.4.6 (2020-02-23) + +### :bug: Bug Fixes + +- auto generate a local png bug ([c54ac67](https://github.com/PicGo/PicGo-Core/commit/c54ac67)) + +## :tada: 1.4.5 (2020-02-23) + +### :sparkles: Features + +- add upload image from URL support ([0d87342](https://github.com/PicGo/PicGo-Core/commit/0d87342)) + +### :package: Chore + +- travis-ci deploy option ([a2a89cd](https://github.com/PicGo/PicGo-Core/commit/a2a89cd)) + +## :tada: 1.4.4 (2019-12-30) + +### :bug: Bug Fixes + +- image_repeated error from smms ([#28](https://github.com/PicGo/PicGo-Core/issues/28)) ([f246b8d](https://github.com/PicGo/PicGo-Core/commit/f246b8d)) + +## :tada: 1.4.3 (2019-12-27) + +### :sparkles: Features + +- add aliyun optionUrl option ([0a3bdea](https://github.com/PicGo/PicGo-Core/commit/0a3bdea)) + +## :tada: 1.4.2 (2019-12-26) + +### :bug: Bug Fixes + +- cli source ([be6cdcc](https://github.com/PicGo/PicGo-Core/commit/be6cdcc)) + +## :tada: 1.4.1 (2019-12-26) + +# :tada: 1.4.0 (2019-12-26) + +### :sparkles: Features + +- add config methods && pluginHandler to ctx ([f9bb9fb](https://github.com/PicGo/PicGo-Core/commit/f9bb9fb)) +- **plugin:** passing environment variables ([50467c7](https://github.com/PicGo/PicGo-Core/commit/50467c7)) + +### :bug: Bug Fixes + +- correct sm.ms err msg ([#18](https://github.com/PicGo/PicGo-Core/issues/18)) ([f0a4e8a](https://github.com/PicGo/PicGo-Core/commit/f0a4e8a)) +- pluginHandler args length error ([e15eac2](https://github.com/PicGo/PicGo-Core/commit/e15eac2)) + +### :package: Chore + +- **types:** added typings field to export type inform… ([#23](https://github.com/PicGo/PicGo-Core/issues/23)) ([8bb16e7](https://github.com/PicGo/PicGo-Core/commit/8bb16e7)) + +## :tada: 1.3.7 (2019-05-12) + +### :bug: Bug Fixes + +- **clipboard:** clipboard image getter error in macOS ([8314604](https://github.com/PicGo/PicGo-Core/commit/8314604)) + +## :tada: 1.3.6 (2019-04-20) + +### :bug: Bug Fixes + +- clipboard image upload under win10 ([48b72ed](https://github.com/PicGo/PicGo-Core/commit/48b72ed)) + +## :tada: 1.3.5 (2019-04-15) + +### :bug: Bug Fixes + +- writing log sometimes disappeared ([d36c0ae](https://github.com/PicGo/PicGo-Core/commit/d36c0ae)) + +### :package: Chore + +- add picgo bump version ([c312302](https://github.com/PicGo/PicGo-Core/commit/c312302)) diff --git a/libs/zhi-picgo-core/README.md b/libs/zhi-picgo-core/README.md new file mode 100644 index 00000000..4edf1029 --- /dev/null +++ b/libs/zhi-picgo-core/README.md @@ -0,0 +1,45 @@ +# zhi-picgo-core + +A tool for picture uploading forked from [PicGO-Core](https://github.com/PicGo/PicGo-Core), for electron usage. + +![picgo-core](https://cdn.jsdelivr.net/gh/Molunerfinn/test/picgo/picgo-core-fix.jpg) + +## Usage + +### Use in node project + +#### Common JS + +```js +const { PicGo } = require("picgo") +``` + +#### ES Module + +```js +import { PicGo } from "picgo" +``` + +#### API usage example + +```js +const picgo = new PicGo() + +// upload a picture from path +picgo.upload(["/xxx/xxx.jpg"]) + +// upload a picture from clipboard +picgo.upload() +``` + +### Use in electron + +```js +const { PicGo } = require("/Users/terwer/Documents/mydocs/zhi-framework/zhi/libs/zhi-picgo-core/dist/index.cjs.js") +const picgo = new PicGo() +console.log(picgo) +``` + +## Documentation + +For more details, you can checkout [documentation](https://picgo.github.io/PicGo-Core-Doc/). diff --git a/libs/zhi-picgo-core/package.json b/libs/zhi-picgo-core/package.json new file mode 100644 index 00000000..2d57a7fd --- /dev/null +++ b/libs/zhi-picgo-core/package.json @@ -0,0 +1,128 @@ +{ + "name": "zhi-picgo-core", + "version": "1.7.0", + "description": "A tool for picture uploading forked from PicGO-Core", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "typings": "dist/index.d.ts", + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "cross-env NODE_ENV=production rimraf ./dist && rollup -c rollup.config.js", + "dev": "cross-env NODE_ENV=development rollup -c rollup.config.js -w", + "start": "node ./bin/picgo", + "lint": "eslint src/**/*.ts --fix" + }, + "keywords": [ + "picture", + "upload", + "util" + ], + "husky": { + "hooks": { + "pre-commit": "npm run lint", + "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" + } + }, + "config": { + "commitizen": { + "path": "./node_modules/cz-customizable" + }, + "cz-customizable": { + "config": "./node_modules/@picgo/bump-version/.cz-config.js" + } + }, + "commitlint": { + "extends": [ + "./node_modules/@picgo/bump-version/commitlint-picgo" + ] + }, + "author": "Terwer", + "license": "MIT", + "devDependencies": { + "@terwer/eslint-config-custom": "workspace:*", + "@picgo/bump-version": "^1.1.2", + "@rollup/plugin-commonjs": "^21.0.0", + "@rollup/plugin-json": "^4.1.0", + "@rollup/plugin-node-resolve": "^13.0.5", + "@rollup/plugin-replace": "^3.0.0", + "@types/cross-spawn": "^6.0.0", + "@types/ejs": "^3.0.5", + "@types/fs-extra": "^5.0.4", + "@types/image-size": "^0.0.29", + "@types/inquirer": "^0.0.42", + "@types/js-yaml": "^4.0.5", + "@types/lodash": "^4.14.175", + "@types/md5": "^2.1.32", + "@types/mime-types": "^2.1.0", + "@types/minimatch": "^3.0.3", + "@types/node": "16.11.7", + "@types/resolve": "^0.0.8", + "@types/rimraf": "^3.0.0", + "@types/tunnel": "^0.0.3", + "@typescript-eslint/eslint-plugin": "3", + "@typescript-eslint/parser": "^3.2.0", + "babel-eslint": "^10.1.0", + "builtins": "^4.0.0", + "conventional-changelog": "^3.0.6", + "copyfiles": "^2.1.0", + "cross-env": "^7.0.3", + "cz-customizable": "^5.10.0", + "eslint": "7", + "eslint-config-standard-with-typescript": "^18.0.2", + "eslint-plugin-import": "2", + "eslint-plugin-node": "11", + "eslint-plugin-promise": "4", + "eslint-plugin-standard": "4", + "execa": "^5.1.1", + "husky": "^1.3.1", + "pre-commit": "^1.2.2", + "rollup": "^2.58.0", + "rollup-plugin-string": "^3.0.0", + "rollup-plugin-terser": "^7.0.2", + "rollup-plugin-typescript2": "^0.34.1", + "typescript": "^4.8.2" + }, + "dependencies": { + "@picgo/i18n": "^1.0.0", + "@picgo/store": "^2.0.2", + "ali-oss": "^6.17.1", + "arraybuffer-to-buffer": "^0.0.7", + "axios": "^0.27.2", + "chalk": "^2.4.1", + "commander": "^8.1.0", + "comment-json": "^2.3.1", + "cross-spawn": "^6.0.5", + "dayjs": "^1.7.4", + "download-git-repo": "^3.0.2", + "ejs": "^2.6.1", + "fs-extra": "^6.0.1", + "globby": "^11.0.4", + "image-size": "^0.8.3", + "inquirer": "^6.0.0", + "is-wsl": "^2.2.0", + "js-yaml": "^4.1.0", + "lodash": "^4.17.21", + "md5": "^2.2.1", + "mime-types": "2.1.33", + "minimatch": "^3.0.4", + "minimist": "^1.2.5", + "qiniu": "^7.2.1", + "resolve": "^1.8.1", + "rimraf": "^3.0.2", + "tunnel": "^0.0.6", + "upyun": "^3.4.6" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/terwer/zhi/zhi-picgo-core.git" + }, + "picBed": { + "current": "github" + }, + "plugins": {}, + "engines": { + "node": ">= 12.0.0" + } +} diff --git a/libs/zhi-picgo-core/rollup.config.js b/libs/zhi-picgo-core/rollup.config.js new file mode 100644 index 00000000..ff1efebd --- /dev/null +++ b/libs/zhi-picgo-core/rollup.config.js @@ -0,0 +1,84 @@ +import { terser } from "rollup-plugin-terser" +import pkg from "./package.json" +import typescript from "rollup-plugin-typescript2" +import commonjs from "@rollup/plugin-commonjs" +import { string } from "rollup-plugin-string" +import json from "@rollup/plugin-json" +import builtins from "builtins" +import replace from "@rollup/plugin-replace" +const version = process.env.VERSION || pkg.version +const sourcemap = "inline" +const banner = `/* + * picgo@${version}, https://github.com/PicGo/PicGo-Core + * (c) 2018-${new Date().getFullYear()} PicGo Group + * Released under the MIT License. + */` +const input = "./src/index.ts" + +const commonOptions = { + // Creating regex of the packages to make sure sub-paths of the + // packages such as `lowdb/adapters/FileSync` are also treated as external + // See https://github.com/rollup/rollup/issues/3684#issuecomment-926558056 + external: [...Object.keys(pkg.dependencies), ...builtins()].map((packageName) => new RegExp(`^${packageName}(/.*)?`)), + plugins: [ + typescript({ + tsconfigOverride: { + compilerOptions: { + target: "ES2017", + module: "ES2015", + }, + }, + }), + // terser(), + commonjs(), + string({ + // Required to be specified + include: ["**/*.applescript", "**/*.ps1", "**/*.sh"], + }), + json(), + replace({ + "process.env.PICGO_VERSION": JSON.stringify(pkg.version), + preventAssignment: true, + }), + ], + input, +} + +const isDev = process.env.NODE_ENV === "development" + +if (!isDev) { + commonOptions.plugins.push(terser()) +} + +/** @type import('rollup').RollupOptions */ +const nodeCjs = { + output: [ + { + file: "dist/index.cjs.js", + format: "cjs", + banner, + sourcemap, + }, + ], + ...commonOptions, +} + +const nodeEsm = { + output: [ + { + file: "dist/index.esm.js", + format: "esm", + banner, + sourcemap, + }, + ], + ...commonOptions, +} + +const bundles = [] +const env = process.env.BUNDLES || "" +if (env.includes("cjs")) bundles.push(nodeCjs) +if (env.includes("esm")) bundles.push(nodeEsm) +if (bundles.length === 0) bundles.push(nodeCjs, nodeEsm) + +export default bundles diff --git a/libs/zhi-picgo-core/src/core/Lifecycle.ts b/libs/zhi-picgo-core/src/core/Lifecycle.ts new file mode 100644 index 00000000..044b3e07 --- /dev/null +++ b/libs/zhi-picgo-core/src/core/Lifecycle.ts @@ -0,0 +1,137 @@ +import { EventEmitter } from "events" +import { ILifecyclePlugins, IPicGo, IPlugin, Undefinable } from "../types" +import { handleUrlEncode } from "../utils/common" +import { IBuildInEvent } from "../utils/enum" +import { createContext } from "../utils/createContext" + +export class Lifecycle extends EventEmitter { + private readonly ctx: IPicGo + + constructor(ctx: IPicGo) { + super() + this.ctx = ctx + } + + async start(input: any[]): Promise { + // ensure every upload process has an unique context + const ctx = createContext(this.ctx) + try { + // images input + if (!Array.isArray(input)) { + throw new Error("Input must be an array.") + } + ctx.input = input + ctx.output = [] + + // lifecycle main + await this.beforeTransform(ctx) + await this.doTransform(ctx) + await this.beforeUpload(ctx) + await this.doUpload(ctx) + await this.afterUpload(ctx) + return ctx + } catch (e: any) { + ctx.log.warn(IBuildInEvent.FAILED) + ctx.emit(IBuildInEvent.UPLOAD_PROGRESS, -1) + ctx.emit(IBuildInEvent.FAILED, e) + ctx.log.error(e) + if (ctx.getConfig>("debug")) { + throw e + } + return ctx + } + } + + private async beforeTransform(ctx: IPicGo): Promise { + ctx.emit(IBuildInEvent.UPLOAD_PROGRESS, 0) + ctx.emit(IBuildInEvent.BEFORE_TRANSFORM, ctx) + ctx.log.info("Before transform") + await this.handlePlugins(ctx.helper.beforeTransformPlugins, ctx) + return ctx + } + + private async doTransform(ctx: IPicGo): Promise { + ctx.emit(IBuildInEvent.UPLOAD_PROGRESS, 30) + const type = ctx.getConfig>("picBed.transformer") || "path" + let currentTransformer = type + let transformer = ctx.helper.transformer.get(type) + if (!transformer) { + transformer = ctx.helper.transformer.get("path") + currentTransformer = "path" + ctx.log.warn(`Can't find transformer - ${type}, switch to default transformer - path`) + } + ctx.log.info(`Transforming... Current transformer is [${currentTransformer}]`) + await transformer?.handle(ctx) + return ctx + } + + private async beforeUpload(ctx: IPicGo): Promise { + ctx.emit(IBuildInEvent.UPLOAD_PROGRESS, 60) + ctx.log.info("Before upload") + ctx.emit(IBuildInEvent.BEFORE_UPLOAD, ctx) + await this.handlePlugins(ctx.helper.beforeUploadPlugins, ctx) + return ctx + } + + private async doUpload(ctx: IPicGo): Promise { + let type = + ctx.getConfig>("picBed.uploader") || + ctx.getConfig>("picBed.current") || + "github" + let uploader = ctx.helper.uploader.get(type) + let currentTransformer = type + if (!uploader) { + type = "github" + currentTransformer = "github" + uploader = ctx.helper.uploader.get("github") + ctx.log.warn(`Can't find uploader - ${type}, switch to default uploader - github`) + } + ctx.log.info(`Uploading... Current uploader is [${currentTransformer}]`) + await uploader?.handle(ctx) + for (const outputImg of ctx.output) { + outputImg.type = type + } + return ctx + } + + private async afterUpload(ctx: IPicGo): Promise { + ctx.emit(IBuildInEvent.AFTER_UPLOAD, ctx) + ctx.emit(IBuildInEvent.UPLOAD_PROGRESS, 100) + await this.handlePlugins(ctx.helper.afterUploadPlugins, ctx) + let msg = "" + const length = ctx.output.length + for (let i = 0; i < length; i++) { + if (typeof ctx.output[i].imgUrl !== "undefined") { + msg += handleUrlEncode(ctx.output[i].imgUrl!) + if (i !== length - 1) { + msg += "\n" + } + } + delete ctx.output[i].base64Image + delete ctx.output[i].buffer + } + ctx.emit(IBuildInEvent.FINISHED, ctx) + ctx.log.success(`\n${msg}`) + return ctx + } + + private async handlePlugins(lifeCyclePlugins: ILifecyclePlugins, ctx: IPicGo): Promise { + const plugins = lifeCyclePlugins.getList() + const pluginNames = lifeCyclePlugins.getIdList() + const lifeCycleName = lifeCyclePlugins.getName() + await Promise.all( + plugins.map(async (plugin: IPlugin, index: number) => { + try { + ctx.log.info(`${lifeCycleName}: ${pluginNames[index]} running`) + await plugin.handle(ctx) + } catch (e) { + ctx.log.error(`${lifeCycleName}: ${pluginNames[index]} error`) + throw e + } + }) + ) + return ctx + } +} + +export default Lifecycle diff --git a/libs/zhi-picgo-core/src/core/PicGo.ts b/libs/zhi-picgo-core/src/core/PicGo.ts new file mode 100644 index 00000000..97826967 --- /dev/null +++ b/libs/zhi-picgo-core/src/core/PicGo.ts @@ -0,0 +1,243 @@ +import fs from "fs-extra" +import path from "path" +import { EventEmitter } from "events" +import { homedir } from "os" +import Commander from "../lib/Commander" +import { Logger } from "../lib/Logger" +import Lifecycle from "./Lifecycle" +import LifecyclePlugins, { setCurrentPluginName } from "../lib/LifecyclePlugins" +import uploaders from "../plugins/uploader" +import transformers from "../plugins/transformer" +import PluginLoader from "../lib/PluginLoader" +import { get, set, unset } from "lodash" +import { + IHelper, + IImgInfo, + IConfig, + IPicGo, + IStringKeyMap, + IPluginLoader, + II18nManager, + IPicGoPlugin, + IPicGoPluginInterface, + IRequest, +} from "../types" +import getClipboardImage from "../utils/getClipboardImage" +import Request from "../lib/Request" +import DB from "../utils/db" +import PluginHandler from "../lib/PluginHandler" +import { IBuildInEvent, IBusEvent } from "../utils/enum" +import { eventBus } from "../utils/eventBus" +import { isConfigKeyInBlackList, isInputConfigValid } from "../utils/common" +import { I18nManager } from "../i18n" + +export class PicGo extends EventEmitter implements IPicGo { + private _config!: IConfig + private lifecycle!: Lifecycle + private db!: DB + private _pluginLoader!: PluginLoader + configPath: string + baseDir!: string + helper!: IHelper + log: Logger + cmd: Commander + output: IImgInfo[] + input: any[] + pluginHandler: PluginHandler + /** + * @deprecated will be removed in v1.5.0+ + * + * use request instead + */ + Request!: Request + i18n!: II18nManager + VERSION: string = process.env.PICGO_VERSION + GUI_VERSION?: string + + get pluginLoader(): IPluginLoader { + return this._pluginLoader + } + + constructor(configPath = "") { + super() + this.configPath = configPath + this.output = [] + this.input = [] + this.helper = { + transformer: new LifecyclePlugins("transformer"), + uploader: new LifecyclePlugins("uploader"), + beforeTransformPlugins: new LifecyclePlugins("beforeTransformPlugins"), + beforeUploadPlugins: new LifecyclePlugins("beforeUploadPlugins"), + afterUploadPlugins: new LifecyclePlugins("afterUploadPlugins"), + } + this.initConfigPath() + this.log = new Logger(this) + this.cmd = new Commander(this) + this.pluginHandler = new PluginHandler(this) + this.initConfig() + this.init() + } + + private initConfigPath(): void { + if (this.configPath === "") { + this.configPath = homedir() + "/.picgo/config.json" + } + if (path.extname(this.configPath).toUpperCase() !== ".JSON") { + this.configPath = "" + throw Error("The configuration file only supports JSON format.") + } + this.baseDir = path.dirname(this.configPath) + const exist = fs.pathExistsSync(this.configPath) + if (!exist) { + fs.ensureFileSync(`${this.configPath}`) + } + } + + private initConfig(): void { + this.db = new DB(this) + this._config = this.db.read(true) as IConfig + } + + private init(): void { + try { + // init 18n at first + this.i18n = new I18nManager(this) + this.Request = new Request(this) + this._pluginLoader = new PluginLoader(this) + // load self plugins + setCurrentPluginName("picgo") + uploaders(this).register(this) + transformers(this).register(this) + setCurrentPluginName("") + // load third-party plugins + this._pluginLoader.load() + this.lifecycle = new Lifecycle(this) + } catch (e: any) { + this.emit(IBuildInEvent.UPLOAD_PROGRESS, -1) + this.log.error(e) + throw e + } + } + + /** + * easily mannually load a plugin + * if provide plugin name, will register plugin by name + * or just instantiate a plugin + */ + use(plugin: IPicGoPlugin, name?: string): IPicGoPluginInterface { + if (name) { + this.pluginLoader.registerPlugin(name, plugin) + return this.pluginLoader.getPlugin(name)! + } else { + const pluginInstance = plugin(this) + return pluginInstance + } + } + + registerCommands(): void { + if (this.configPath !== "") { + this.cmd.init() + this.cmd.loadCommands() + } + } + + getConfig(name?: string): T { + if (!name) { + return this._config as unknown as T + } else { + return get(this._config, name) + } + } + + saveConfig(config: IStringKeyMap): void { + if (!isInputConfigValid(config)) { + this.log.warn("the format of config is invalid, please provide object") + return + } + this.setConfig(config) + this.db.saveConfig(config) + } + + removeConfig(key: string, propName: string): void { + if (!key || !propName) return + if (isConfigKeyInBlackList(key)) { + this.log.warn(`the config.${key} can't be removed`) + return + } + this.unsetConfig(key, propName) + this.db.unset(key, propName) + } + + setConfig(config: IStringKeyMap): void { + if (!isInputConfigValid(config)) { + this.log.warn("the format of config is invalid, please provide object") + return + } + Object.keys(config).forEach((name: string) => { + if (isConfigKeyInBlackList(name)) { + this.log.warn(`the config.${name} can't be modified`) + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete config[name] + } + set(this._config, name, config[name]) + eventBus.emit(IBusEvent.CONFIG_CHANGE, { + configName: name, + value: config[name], + }) + }) + } + + unsetConfig(key: string, propName: string): void { + if (!key || !propName) return + if (isConfigKeyInBlackList(key)) { + this.log.warn(`the config.${key} can't be unset`) + return + } + unset(this.getConfig(key), propName) + } + + get request(): IRequest["request"] { + return this.Request.request.bind(this.Request) + } + + async upload(input?: any[]): Promise { + if (this.configPath === "") { + this.log.error("The configuration file only supports JSON format.") + return [] + } + // upload from clipboard + if (input === undefined || input.length === 0) { + try { + const { imgPath, shouldKeepAfterUploading } = await getClipboardImage(this) + if (imgPath === "no image") { + throw new Error("image not found in clipboard") + } else { + this.once(IBuildInEvent.FAILED, () => { + if (!shouldKeepAfterUploading) { + // 删除 picgo 生成的图片文件,例如 `~/.picgo/20200621205720.png` + fs.remove(imgPath).catch((e) => { + this.log.error(e) + }) + } + }) + this.once("finished", () => { + if (!shouldKeepAfterUploading) { + fs.remove(imgPath).catch((e) => { + this.log.error(e) + }) + } + }) + const { output } = await this.lifecycle.start([imgPath]) + return output + } + } catch (e) { + this.emit(IBuildInEvent.FAILED, e) + throw e + } + } else { + // upload from path + const { output } = await this.lifecycle.start(input) + return output + } + } +} diff --git a/libs/zhi-picgo-core/src/custom-env.d.ts b/libs/zhi-picgo-core/src/custom-env.d.ts new file mode 100644 index 00000000..1c93493e --- /dev/null +++ b/libs/zhi-picgo-core/src/custom-env.d.ts @@ -0,0 +1,19 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +declare module "*.sh" { + const src: string + export default src +} +declare module "*.applescript" { + const src: string + export default src +} +declare module "*.ps1" { + const src: string + export default src +} + +declare namespace NodeJS { + interface ProcessEnv { + readonly PICGO_VERSION: string + } +} diff --git a/libs/zhi-picgo-core/src/i18n/en.ts b/libs/zhi-picgo-core/src/i18n/en.ts new file mode 100644 index 00000000..fc198224 --- /dev/null +++ b/libs/zhi-picgo-core/src/i18n/en.ts @@ -0,0 +1,111 @@ +import { ILocales } from "./zh-CN" + +/* eslint-disable no-template-curly-in-string */ +export const EN: ILocales = { + UPLOAD_FAILED: "Upload failed", + CHECK_SETTINGS: "Please check your settings", + CHECK_SETTINGS_AND_NETWORK: "Please check your settings and network", + UPLOAD_FAILED_REASON: "Error code: ${code}, please open the browser and paste the address to see the reason", + SERVER_ERROR: "Server error, please try again later", + AUTH_FAILED: "Authentication failed", + + // smms + PICBED_SMMS: "SM.MS", + PICBED_SMMS_TOKEN: "Set Token", + PICBED_SMMS_BACKUP_DOMAIN: "Set Backup Upload Domain", + PICBED_SMMS_MESSAGE_BACKUP_DOMAIN: "Ex. smms.app", + + // Ali-cloud + PICBED_ALICLOUD: "Ali Cloud", + PICBED_ALICLOUD_ACCESSKEYID: "Set KeyId", + PICBED_ALICLOUD_ACCESSKEYSECRET: "Set KeySecret", + PICBED_ALICLOUD_BUCKET: "Set Bucket", + PICBED_ALICLOUD_AREA: "Set Area", + PICBED_ALICLOUD_PATH: "Set Path", + PICBED_ALICLOUD_CUSTOMURL: "Set Custom URL", + PICBED_ALICLOUD_OPTIONS: "Set URL Suffix", + PICBED_ALICLOUD_MESSAGE_AREA: "Ex. oss-cn-beijing", + PICBED_ALICLOUD_MESSAGE_PATH: "Ex. test/", + PICBED_ALICLOUD_MESSAGE_OPTIONS: "Ex. ?x-oss-process=xxx", + PICBED_ALICLOUD_MESSAGE_CUSTOMURL: "Ex. https://test.com", + + // Tencent-cloud + PICBED_TENCENTCLOUD: "Tencent Cloud", + PICBED_TENCENTCLOUD_VERSION: "Choose COS version", + PICBED_TENCENTCLOUD_SECRETID: "Set SecretId", + PICBED_TENCENTCLOUD_SECRETKEY: "Set SecretKey", + PICBED_TENCENTCLOUD_APPID: "Set AppId", + PICBED_TENCENTCLOUD_BUCKET: "Set Bucket", + PICBED_TENCENTCLOUD_AREA: "Set Area", + PICBED_TENCENTCLOUD_PATH: "Set Path", + PICBED_TENCENTCLOUD_OPTIONS: "Set URL Suffix", + PICBED_TENCENTCLOUD_CUSTOMURL: "Set Custom URL", + PICBED_TENCENTCLOUD_MESSAGE_APPID: "Ex. 1234567890", + PICBED_TENCENTCLOUD_MESSAGE_AREA: "Ex. ap-beijing", + PICBED_TENCENTCLOUD_MESSAGE_PATH: "Ex. test/", + PICBED_TENCENTCLOUD_MESSAGE_CUSTOMURL: "Ex. http://test.com", + PICBED_TENCENTCLOUD_MESSAGE_OPTIONS: "Ex. ?imageMogr2", + + // GitHub + PICBED_GITHUB: "GitHub", + PICBED_GITHUB_TOKEN: "Set Token", + PICBED_GITHUB_REPO: "Set Repo Name", + PICBED_GITHUB_PATH: "Set Path", + PICBED_GITHUB_BRANCH: "Set Branch", + PICBED_GITHUB_CUSTOMURL: "Set Custom URL", + PICBED_GITHUB_MESSAGE_REPO: "Ex. username/repo", + PICBED_GITHUB_MESSAGE_BRANCH: "Ex. main", + PICBED_GITHUB_MESSAGE_PATH: "Ex. test/", + PICBED_GITHUB_MESSAGE_CUSTOMURL: "Ex. https://test.com", + + // qiniu + PICBED_QINIU: "Qiniu", + PICBED_QINIU_ACCESSKEY: "Set AccessKey", + PICBED_QINIU_SECRETKEY: "Set SecretKey", + PICBED_QINIU_BUCKET: "Set Bucket", + PICBED_QINIU_PATH: "Set Path", + PICBED_QINIU_URL: "Set URL", + PICBED_QINIU_OPTIONS: "Set URL Suffix", + PICBED_QINIU_AREA: "Set Area", + PICBED_QINIU_MESSAGE_PATH: "Ex. test/", + PICBED_QINIU_MESSAGE_AREA: "Ex. z0", + PICBED_QINIU_MESSAGE_OPTIONS: "Ex. ?imageslim", + PICBED_QINIU_MESSAGE_URL: "Ex. https://xxx.yyy.glb.clouddn.com", + + // imgur + PICBED_IMGUR: "Imgur", + PICBED_IMGUR_CLIENTID: "Set ClientId", + PICBED_IMGUR_PROXY: "Set Proxy", + PICBED_IMGUR_MESSAGE_PROXY: "Ex. http://127.0.0.1:1080", + + // upyun + PICBED_UPYUN: "Upyun", + PICBED_UPYUN_BUCKET: "Set Bucket", + PICBED_UPYUN_OPERATOR: "Set Operator", + PICBED_UPYUN_PASSWORD: "Set Operator Password", + PICBED_UPYUN_PATH: "Set Path", + PICBED_UPYUN_URL: "Set URL", + PICBED_UPYUN_OPTIONS: "Set URL Suffix", + PICBED_UPYUN_MESSAGE_OPERATOR: "Ex. me", + PICBED_UPYUN_MESSAGE_PASSWORD: "Please type the operator password", + PICBED_UPYUN_MESSAGE_URL: "Ex. http://xxx.test.upcdn.net", + PICBED_UPYUN_MESSAGE_OPTIONS: "Ex. !imgslim", + PICBED_UPYUN_MESSAGE_PATH: "Ex. test/", + + // Plugin Handler + PLUGIN_HANDLER_PLUGIN_INSTALL_SUCCESS: "Plugin installed successfully", + PLUGIN_HANDLER_PLUGIN_INSTALL_FAILED: "Plugin installation failed", + PLUGIN_HANDLER_PLUGIN_INSTALL_FAILED_REASON: + "Plugin installation failed, error code is ${code}, error log is \n ${data}", + PLUGIN_HANDLER_PLUGIN_INSTALL_FAILED_PATH: + "Plugin installation failed, please enter a valid plugin name or valid installation path", + PLUGIN_HANDLER_PLUGIN_UNINSTALL_SUCCESS: "Plugin uninstalled successfully", + PLUGIN_HANDLER_PLUGIN_UNINSTALL_FAILED: "Plugin uninstall failed", + PLUGIN_HANDLER_PLUGIN_UNINSTALL_FAILED_REASON: + "Plugin uninstall failed, error code is ${code}, error log is \n ${data}", + PLUGIN_HANDLER_PLUGIN_UNINSTALL_FAILED_VALID: "Plugin uninstall failed, please enter a valid plugin name", + PLUGIN_HANDLER_PLUGIN_UPDATE_SUCCESS: "Plugin updated successfully", + PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED: "Plugin update failed", + PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED_REASON: "Plugin update failed, error code is ${code}, error log is \n ${data}", + PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED_VALID: "Plugin update failed, please enter a valid plugin name", +} diff --git a/libs/zhi-picgo-core/src/i18n/index.ts b/libs/zhi-picgo-core/src/i18n/index.ts new file mode 100644 index 00000000..018970a9 --- /dev/null +++ b/libs/zhi-picgo-core/src/i18n/index.ts @@ -0,0 +1,101 @@ +import { ZH_CN, ILocalesKey, ILocales } from "./zh-CN" +import { merge } from "lodash" +import { IPicGo } from "../types" +import path from "path" +import fs from "fs-extra" +import yaml from "js-yaml" + +import { ObjectAdapter, I18n } from "@picgo/i18n" +import { IStringKeyMap, II18nManager } from "../types/index" +import { ILocale } from "@picgo/i18n/dist/types" +import { EN } from "./en" +import { ZH_TW } from "./zh-TW" + +const languageList: IStringKeyMap> = { + "zh-CN": ZH_CN, + "zh-TW": ZH_TW, + en: EN, +} + +class I18nManager implements II18nManager { + private readonly i18n: I18n + private readonly objectAdapter: ObjectAdapter + private readonly ctx: IPicGo + constructor(ctx: IPicGo) { + this.ctx = ctx + this.objectAdapter = new ObjectAdapter(languageList) + let language = this.ctx.getConfig("settings.language") || "zh-CN" + if (!languageList[language]) { + language = "zh-CN" // use default + } + this.i18n = new I18n({ + adapter: this.objectAdapter, + defaultLanguage: language, + }) + this.loadOutterI18n() + } + + private loadOutterI18n(): void { + const i18nFolder = this.getOutterI18nFolder() + const files = fs.readdirSync(i18nFolder, { + withFileTypes: true, + }) + files.forEach((file) => { + if (file.isFile() && file.name.endsWith(".yml")) { + const i18nFilePath = path.join(i18nFolder, file.name) + const i18nFile = fs.readFileSync(i18nFilePath, "utf8") + try { + const i18nFileObj = yaml.load(i18nFile) as ILocales + languageList[file.name.replace(/\.yml$/, "")] = i18nFileObj + } catch (e) { + console.error(e) + } + } + }) + } + + private getOutterI18nFolder(): string { + const i18nFolder = path.join(this.ctx.baseDir, "i18n-cli") + if (!fs.pathExistsSync(i18nFolder)) { + fs.ensureDirSync(i18nFolder) + } + return i18nFolder + } + + translate(key: ILocalesKey | T, args?: IStringKeyMap): string { + return this.i18n.translate(key, args) || key + } + + setLanguage(language: string): void { + this.i18n.setLanguage(language) + this.ctx.saveConfig({ + "settings.language": language, + }) + } + + addLocale(language: string, locales: ILocale): boolean { + const originLocales = this.objectAdapter.getLocale(language) + if (!originLocales) { + return false + } + const newLocales = merge(originLocales, locales) + this.objectAdapter.setLocale(language, newLocales) + return true + } + + addLanguage(language: string, locales: ILocale): boolean { + const originLocales = this.objectAdapter.getLocale(language) + if (originLocales) { + return false + } + this.objectAdapter.setLocale(language, locales) + languageList[language] = locales + return true + } + + getLanguageList(): string[] { + return Object.keys(languageList) + } +} + +export { I18nManager } diff --git a/libs/zhi-picgo-core/src/i18n/zh-CN.ts b/libs/zhi-picgo-core/src/i18n/zh-CN.ts new file mode 100644 index 00000000..48b4a11e --- /dev/null +++ b/libs/zhi-picgo-core/src/i18n/zh-CN.ts @@ -0,0 +1,109 @@ +/* eslint-disable no-template-curly-in-string */ +export const ZH_CN = { + UPLOAD_FAILED: "上传失败", + CHECK_SETTINGS: "请检查你的配置项是否正确", + CHECK_SETTINGS_AND_NETWORK: "请检查你的配置项以及网络", + UPLOAD_FAILED_REASON: "错误码:${code},请打开浏览器粘贴地址查看相关原因", + SERVER_ERROR: "服务端出错,请重试", + AUTH_FAILED: "认证失败", + + // smms + PICBED_SMMS: "SM.MS", + PICBED_SMMS_TOKEN: "设定Token", + PICBED_SMMS_BACKUP_DOMAIN: "备用上传域名", + PICBED_SMMS_MESSAGE_BACKUP_DOMAIN: "例如 smms.app", + + // Ali-cloud + PICBED_ALICLOUD: "阿里云OSS", + PICBED_ALICLOUD_ACCESSKEYID: "设定KeyId", + PICBED_ALICLOUD_ACCESSKEYSECRET: "设定KeySecret", + PICBED_ALICLOUD_BUCKET: "设定Bucket", + PICBED_ALICLOUD_AREA: "设定存储区域", + PICBED_ALICLOUD_PATH: "设定存储路径", + PICBED_ALICLOUD_CUSTOMURL: "设定自定义域名", + PICBED_ALICLOUD_OPTIONS: "设定网址后缀", + PICBED_ALICLOUD_MESSAGE_AREA: "例如:oss-cn-beijing", + PICBED_ALICLOUD_MESSAGE_PATH: "例如:test/", + PICBED_ALICLOUD_MESSAGE_OPTIONS: "例如:?x-oss-process=xxx", + PICBED_ALICLOUD_MESSAGE_CUSTOMURL: "例如:https://test.com", + + // Tencent-cloud + PICBED_TENCENTCLOUD: "腾讯云COS", + PICBED_TENCENTCLOUD_VERSION: "COS版本", + PICBED_TENCENTCLOUD_SECRETID: "设定SecretId", + PICBED_TENCENTCLOUD_SECRETKEY: "设定SecretKey", + PICBED_TENCENTCLOUD_APPID: "设定AppId", + PICBED_TENCENTCLOUD_BUCKET: "设定Bucket", + PICBED_TENCENTCLOUD_AREA: "设定存储区域", + PICBED_TENCENTCLOUD_PATH: "设定存储路径", + PICBED_TENCENTCLOUD_OPTIONS: "设定网址后缀", + PICBED_TENCENTCLOUD_CUSTOMURL: "设定自定义域名", + PICBED_TENCENTCLOUD_MESSAGE_APPID: "例如:1234567890", + PICBED_TENCENTCLOUD_MESSAGE_AREA: "例如:ap-beijing", + PICBED_TENCENTCLOUD_MESSAGE_PATH: "例如:test/", + PICBED_TENCENTCLOUD_MESSAGE_CUSTOMURL: "例如:https://test.com", + PICBED_TENCENTCLOUD_MESSAGE_OPTIONS: "例如:?imageMogr2", + + // GitHub + PICBED_GITHUB: "GitHub", + PICBED_GITHUB_TOKEN: "设定Token", + PICBED_GITHUB_REPO: "设定仓库名", + PICBED_GITHUB_PATH: "设定存储路径", + PICBED_GITHUB_BRANCH: "设定分支名", + PICBED_GITHUB_CUSTOMURL: "设定自定义域名", + PICBED_GITHUB_MESSAGE_REPO: "格式:username/repo", + PICBED_GITHUB_MESSAGE_BRANCH: "例如:main", + PICBED_GITHUB_MESSAGE_PATH: "例如:test/", + PICBED_GITHUB_MESSAGE_CUSTOMURL: "例如:https://test.com", + + // qiniu + PICBED_QINIU: "七牛云", + PICBED_QINIU_ACCESSKEY: "设定AccessKey", + PICBED_QINIU_SECRETKEY: "设定SecretKey", + PICBED_QINIU_BUCKET: "设定Bucket", + PICBED_QINIU_PATH: "设定存储路径", + PICBED_QINIU_URL: "设定访问网址", + PICBED_QINIU_OPTIONS: "设定网址后缀", + PICBED_QINIU_AREA: "设定存储区域", + PICBED_QINIU_MESSAGE_PATH: "例如:test/", + PICBED_QINIU_MESSAGE_AREA: "例如:z0", + PICBED_QINIU_MESSAGE_OPTIONS: "例如:?imageslim", + PICBED_QINIU_MESSAGE_URL: "例如:https://xxx.yyy.glb.clouddn.com", + + // imgur + PICBED_IMGUR: "Imgur", + PICBED_IMGUR_CLIENTID: "设定ClientId", + PICBED_IMGUR_PROXY: "设定代理", + PICBED_IMGUR_MESSAGE_PROXY: "例如:http://127.0.0.1:1080", + + // upyun + PICBED_UPYUN: "又拍云", + PICBED_UPYUN_BUCKET: "设定Bucket", + PICBED_UPYUN_OPERATOR: "设定操作员", + PICBED_UPYUN_PASSWORD: "设定操作员密码", + PICBED_UPYUN_PATH: "设定存储路径", + PICBED_UPYUN_URL: "设定加速域名", + PICBED_UPYUN_OPTIONS: "设定网址后缀", + PICBED_UPYUN_MESSAGE_OPERATOR: "例如:me", + PICBED_UPYUN_MESSAGE_PASSWORD: "输入操作员密码", + PICBED_UPYUN_MESSAGE_URL: "例如:http://xxx.test.upcdn.net", + PICBED_UPYUN_MESSAGE_OPTIONS: "例如:!imgslim", + PICBED_UPYUN_MESSAGE_PATH: "例如:test/", + + // Plugin Handler + PLUGIN_HANDLER_PLUGIN_INSTALL_SUCCESS: "插件安装成功", + PLUGIN_HANDLER_PLUGIN_INSTALL_FAILED: "插件安装失败", + PLUGIN_HANDLER_PLUGIN_INSTALL_FAILED_REASON: "插件安装失败,失败码为${code},错误日志为 \n ${data}", + PLUGIN_HANDLER_PLUGIN_INSTALL_FAILED_PATH: "插件安装失败,请输入合法插件名或合法安装路径", + PLUGIN_HANDLER_PLUGIN_UNINSTALL_SUCCESS: "插件卸载成功", + PLUGIN_HANDLER_PLUGIN_UNINSTALL_FAILED: "插件卸载失败", + PLUGIN_HANDLER_PLUGIN_UNINSTALL_FAILED_REASON: "插件卸载失败,失败码为${code},错误日志为 \n ${data}", + PLUGIN_HANDLER_PLUGIN_UNINSTALL_FAILED_VALID: "插件卸载失败,请输入合法插件名", + PLUGIN_HANDLER_PLUGIN_UPDATE_SUCCESS: "插件更新成功", + PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED: "插件更新失败", + PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED_REASON: "插件更新失败,失败码为${code},错误日志为 \n ${data}", + PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED_VALID: "插件更新失败,请输入合法插件名", +} + +export type ILocalesKey = keyof typeof ZH_CN +export type ILocales = typeof ZH_CN diff --git a/libs/zhi-picgo-core/src/i18n/zh-TW.ts b/libs/zhi-picgo-core/src/i18n/zh-TW.ts new file mode 100644 index 00000000..07caed5e --- /dev/null +++ b/libs/zhi-picgo-core/src/i18n/zh-TW.ts @@ -0,0 +1,108 @@ +import { ILocales } from "./zh-CN" + +/* eslint-disable no-template-curly-in-string */ +export const ZH_TW: ILocales = { + UPLOAD_FAILED: "上傳失敗", + CHECK_SETTINGS: "請檢查你的設定是否正確", + CHECK_SETTINGS_AND_NETWORK: "請檢查你的設定及網路", + UPLOAD_FAILED_REASON: "錯誤碼:${code},請打開瀏覽器貼上地址查看相關原因", + SERVER_ERROR: "伺服器出錯,請重試", + AUTH_FAILED: "認證失敗", + + // smms + PICBED_SMMS: "SM.MS", + PICBED_SMMS_TOKEN: "設定Token", + PICBED_SMMS_BACKUP_DOMAIN: "備用上傳網址", + PICBED_SMMS_MESSAGE_BACKUP_DOMAIN: "例如 smms.app", + + // Ali-cloud + PICBED_ALICLOUD: "阿里云OSS", + PICBED_ALICLOUD_ACCESSKEYID: "設定KeyId", + PICBED_ALICLOUD_ACCESSKEYSECRET: "設定KeySecret", + PICBED_ALICLOUD_BUCKET: "設定Bucket", + PICBED_ALICLOUD_AREA: "設定儲存區域", + PICBED_ALICLOUD_PATH: "設定儲存路徑", + PICBED_ALICLOUD_CUSTOMURL: "設定自訂網址", + PICBED_ALICLOUD_OPTIONS: "設定網址後綴", + PICBED_ALICLOUD_MESSAGE_AREA: "例如:oss-cn-beijing", + PICBED_ALICLOUD_MESSAGE_PATH: "例如:test/", + PICBED_ALICLOUD_MESSAGE_OPTIONS: "例如:?x-oss-process=xxx", + PICBED_ALICLOUD_MESSAGE_CUSTOMURL: "例如:https://test.com", + + // Tencent-cloud + PICBED_TENCENTCLOUD: "騰訊云COS", + PICBED_TENCENTCLOUD_VERSION: "COS版本", + PICBED_TENCENTCLOUD_SECRETID: "設定SecretId", + PICBED_TENCENTCLOUD_SECRETKEY: "設定SecretKey", + PICBED_TENCENTCLOUD_APPID: "設定AppId", + PICBED_TENCENTCLOUD_BUCKET: "設定Bucket", + PICBED_TENCENTCLOUD_AREA: "設定儲存區域", + PICBED_TENCENTCLOUD_PATH: "設定儲存路徑", + PICBED_TENCENTCLOUD_OPTIONS: "設定網址後綴", + PICBED_TENCENTCLOUD_CUSTOMURL: "設定自訂網址", + PICBED_TENCENTCLOUD_MESSAGE_APPID: "例如:1234567890", + PICBED_TENCENTCLOUD_MESSAGE_AREA: "例如:ap-beijing", + PICBED_TENCENTCLOUD_MESSAGE_PATH: "例如:test/", + PICBED_TENCENTCLOUD_MESSAGE_CUSTOMURL: "例如:https://test.com", + PICBED_TENCENTCLOUD_MESSAGE_OPTIONS: "例如:?imageMogr2", + + // GitHub + PICBED_GITHUB: "GitHub", + PICBED_GITHUB_TOKEN: "設定Token", + PICBED_GITHUB_REPO: "設定倉庫名稱", + PICBED_GITHUB_PATH: "設定儲存路徑", + PICBED_GITHUB_BRANCH: "設定分支名稱", + PICBED_GITHUB_CUSTOMURL: "設定自訂網址", + PICBED_GITHUB_MESSAGE_REPO: "格式:username/repo", + PICBED_GITHUB_MESSAGE_BRANCH: "例如:main", + PICBED_GITHUB_MESSAGE_PATH: "例如:test/", + PICBED_GITHUB_MESSAGE_CUSTOMURL: "例如:https://test.com", + + // qiniu + PICBED_QINIU: "七牛云", + PICBED_QINIU_ACCESSKEY: "設定AccessKey", + PICBED_QINIU_SECRETKEY: "設定SecretKey", + PICBED_QINIU_BUCKET: "設定Bucket", + PICBED_QINIU_PATH: "設定儲存路徑", + PICBED_QINIU_URL: "設定訪問網址", + PICBED_QINIU_OPTIONS: "設定網址後綴", + PICBED_QINIU_AREA: "設定儲存區域", + PICBED_QINIU_MESSAGE_PATH: "例如:test/", + PICBED_QINIU_MESSAGE_AREA: "例如:z0", + PICBED_QINIU_MESSAGE_OPTIONS: "例如:?imageslim", + PICBED_QINIU_MESSAGE_URL: "例如:https://xxx.yyy.glb.clouddn.com", + + // imgur + PICBED_IMGUR: "Imgur", + PICBED_IMGUR_CLIENTID: "設定ClientId", + PICBED_IMGUR_PROXY: "設定PROXY", + PICBED_IMGUR_MESSAGE_PROXY: "例如:http://127.0.0.1:1080", + + // upyun + PICBED_UPYUN: "又拍云", + PICBED_UPYUN_BUCKET: "設定Bucket", + PICBED_UPYUN_OPERATOR: "設定操作員", + PICBED_UPYUN_PASSWORD: "設定操作員密碼", + PICBED_UPYUN_PATH: "設定儲存路徑", + PICBED_UPYUN_URL: "設定加速網址", + PICBED_UPYUN_OPTIONS: "設定網址後綴", + PICBED_UPYUN_MESSAGE_OPERATOR: "例如:me", + PICBED_UPYUN_MESSAGE_PASSWORD: "輸入操作員密碼", + PICBED_UPYUN_MESSAGE_URL: "例如:http://xxx.test.upcdn.net", + PICBED_UPYUN_MESSAGE_OPTIONS: "例如:!imgslim", + PICBED_UPYUN_MESSAGE_PATH: "例如:test/", + + // Plugin Handler + PLUGIN_HANDLER_PLUGIN_INSTALL_SUCCESS: "插件安裝成功", + PLUGIN_HANDLER_PLUGIN_INSTALL_FAILED: "插件安裝失敗", + PLUGIN_HANDLER_PLUGIN_INSTALL_FAILED_REASON: "插件安裝失敗,失敗碼為${code},錯誤紀錄為 \n ${data}", + PLUGIN_HANDLER_PLUGIN_INSTALL_FAILED_PATH: "插件安裝失敗,請輸入正確的插件名稱或正確的安裝路徑", + PLUGIN_HANDLER_PLUGIN_UNINSTALL_SUCCESS: "插件卸載成功", + PLUGIN_HANDLER_PLUGIN_UNINSTALL_FAILED: "插件卸載失敗", + PLUGIN_HANDLER_PLUGIN_UNINSTALL_FAILED_REASON: "插件卸載失敗,失敗碼為${code},錯誤紀錄為 \n ${data}", + PLUGIN_HANDLER_PLUGIN_UNINSTALL_FAILED_VALID: "插件卸載失敗,請輸入正確的插件名稱", + PLUGIN_HANDLER_PLUGIN_UPDATE_SUCCESS: "插件更新成功", + PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED: "插件更新失敗", + PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED_REASON: "插件更新失敗,失敗碼為${code},錯誤紀錄為 \n ${data}", + PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED_VALID: "插件更新失敗,請輸入正確的插件名稱", +} diff --git a/libs/zhi-picgo-core/src/index.ts b/libs/zhi-picgo-core/src/index.ts new file mode 100644 index 00000000..788bfc6b --- /dev/null +++ b/libs/zhi-picgo-core/src/index.ts @@ -0,0 +1,11 @@ +export { PicGo } from "./core/PicGo" +export { Lifecycle } from "./core/Lifecycle" + +export { Logger } from "./lib/Logger" +export { PluginHandler } from "./lib/PluginHandler" +export { LifecyclePlugins } from "./lib/LifecyclePlugins" +export { Commander } from "./lib/Commander" +export { PluginLoader } from "./lib/PluginLoader" +export { Request } from "./lib/Request" + +export * from "./types" diff --git a/libs/zhi-picgo-core/src/lib/Commander.ts b/libs/zhi-picgo-core/src/lib/Commander.ts new file mode 100644 index 00000000..cf5d80c7 --- /dev/null +++ b/libs/zhi-picgo-core/src/lib/Commander.ts @@ -0,0 +1,99 @@ +/* eslint-disable @typescript-eslint/no-misused-promises */ +import { Command } from "commander" +import inquirer, { Inquirer } from "inquirer" +import { IPlugin, ICommander, IPicGo } from "../types" +import commanders from "../plugins/commander" +import { getCurrentPluginName } from "./LifecyclePlugins" + +export class Commander implements ICommander { + private readonly name = "commander" + static currentPlugin: string | null + private readonly list: Map = new Map() + private readonly pluginIdMap: Map = new Map() + private readonly ctx: IPicGo + + program: Command + inquirer: Inquirer + + constructor(ctx: IPicGo) { + this.program = new Command() + this.inquirer = inquirer + this.ctx = ctx + } + + getName(): string { + return this.name + } + + init(): void { + this.program + .version(process.env.PICGO_VERSION, "-v, --version") + .option("-d, --debug", "debug mode", () => { + this.ctx.setConfig({ + debug: true, + }) + }) + .option("-s, --silent", "silent mode", () => { + this.ctx.setConfig({ + silent: true, + }) + }) + .on("command:*", () => { + this.ctx.log.error( + `Invalid command: ${this.program.args.join(" ")}\nSee --help for a list of available commands.` + ) + process.exit(1) + }) + + // built-in commands + commanders(this.ctx) + } + + register(id: string, plugin: IPlugin): void { + if (!id) throw new TypeError("name is required!") + if (typeof plugin.handle !== "function") throw new TypeError("plugin.handle must be a function!") + if (this.list.has(id)) throw new TypeError(`${this.name} plugin duplicate id: ${id}!`) + this.list.set(id, plugin) + const currentPluginName = getCurrentPluginName() + if (currentPluginName !== null) { + if (this.pluginIdMap.has(currentPluginName)) { + this.pluginIdMap.get(currentPluginName)?.push(id) + } else { + this.pluginIdMap.set(currentPluginName, [id]) + } + } + } + + unregister(pluginName: string): void { + if (this.pluginIdMap.has(pluginName)) { + const pluginList = this.pluginIdMap.get(pluginName) + pluginList?.forEach((plugin: string) => { + this.list.delete(plugin) + }) + } + } + + loadCommands(): void { + this.getList().forEach((item: IPlugin) => { + try { + item.handle(this.ctx) + } catch (e: any) { + this.ctx.log.error(e) + } + }) + } + + get(id: string): IPlugin | undefined { + return this.list.get(id) + } + + getList(): IPlugin[] { + return [...this.list.values()] + } + + getIdList(): string[] { + return [...this.list.keys()] + } +} + +export default Commander diff --git a/libs/zhi-picgo-core/src/lib/LifecyclePlugins.ts b/libs/zhi-picgo-core/src/lib/LifecyclePlugins.ts new file mode 100644 index 00000000..8641fda2 --- /dev/null +++ b/libs/zhi-picgo-core/src/lib/LifecyclePlugins.ts @@ -0,0 +1,63 @@ +import { IPlugin, ILifecyclePlugins } from "../types" + +export class LifecyclePlugins implements ILifecyclePlugins { + static currentPlugin: string | null + private readonly list: Map + private readonly pluginIdMap: Map + private readonly name: string + + constructor(name: string) { + this.name = name + this.list = new Map() + this.pluginIdMap = new Map() + } + + register(id: string, plugin: IPlugin): void { + if (!id) throw new TypeError("id is required!") + if (typeof plugin.handle !== "function") throw new TypeError("plugin.handle must be a function!") + if (this.list.has(id)) throw new TypeError(`${this.name} duplicate id: ${id}!`) + this.list.set(id, plugin) + if (LifecyclePlugins.currentPlugin) { + if (this.pluginIdMap.has(LifecyclePlugins.currentPlugin)) { + this.pluginIdMap.get(LifecyclePlugins.currentPlugin)?.push(id) + } else { + this.pluginIdMap.set(LifecyclePlugins.currentPlugin, [id]) + } + } + } + + unregister(pluginName: string): void { + if (this.pluginIdMap.has(pluginName)) { + const pluginList = this.pluginIdMap.get(pluginName) + pluginList?.forEach((plugin: string) => { + this.list.delete(plugin) + }) + } + } + + getName(): string { + return this.name + } + + get(id: string): IPlugin | undefined { + return this.list.get(id) + } + + getList(): IPlugin[] { + return [...this.list.values()] + } + + getIdList(): string[] { + return [...this.list.keys()] + } +} + +export const setCurrentPluginName = (name: string | null = null): void => { + LifecyclePlugins.currentPlugin = name +} + +export const getCurrentPluginName = (): string | null => { + return LifecyclePlugins.currentPlugin +} + +export default LifecyclePlugins diff --git a/libs/zhi-picgo-core/src/lib/Logger.ts b/libs/zhi-picgo-core/src/lib/Logger.ts new file mode 100644 index 00000000..48f676f6 --- /dev/null +++ b/libs/zhi-picgo-core/src/lib/Logger.ts @@ -0,0 +1,138 @@ +import chalk from "chalk" +import dayjs from "dayjs" +import fs from "fs-extra" +import path from "path" +import util from "util" +import { ILogType } from "../utils/enum" +import { ILogArgvType, ILogArgvTypeWithError, Undefinable, ILogColor, ILogger, IPicGo } from "../types" +import { forceNumber, isDev } from "../utils/common" + +export class Logger implements ILogger { + private readonly level = { + [ILogType.success]: "green", + [ILogType.info]: "blue", + [ILogType.warn]: "yellow", + [ILogType.error]: "red", + } + + private readonly ctx: IPicGo + private logLevel!: string + private logPath!: string + constructor(ctx: IPicGo) { + this.ctx = ctx + } + + private handleLog(type: ILogType, ...msg: ILogArgvTypeWithError[]): void { + // check config.silent + if (!this.ctx.getConfig>("silent")) { + const logHeader = chalk[this.level[type] as ILogColor](`[PicGo ${type.toUpperCase()}]:`) + console.log(logHeader, ...msg) + this.logLevel = this.ctx.getConfig("settings.logLevel") + this.logPath = + this.ctx.getConfig>("settings.logPath") || path.join(this.ctx.baseDir, "./picgo.log") + setTimeout(() => { + // fix log file is too large, now the log file's default size is 10 MB + try { + const result = this.checkLogFileIsLarge(this.logPath) + if (result.isLarge) { + const warningMsg = `Log file is too large (> ${ + result.logFileSizeLimit! / 1024 / 1024 || "10" + } MB), recreate log file` + console.log(chalk.yellow("[PicGo WARN]:"), warningMsg) + this.recreateLogFile(this.logPath) + msg.unshift(warningMsg) + } + this.handleWriteLog(this.logPath, type, ...msg) + } catch (e) { + // why??? + console.error("[PicGo Error] on checking log file size", e) + } + }, 0) + } + } + + private checkLogFileIsLarge(logPath: string): { + isLarge: boolean + logFileSize?: number + logFileSizeLimit?: number + } { + if (fs.existsSync(logPath)) { + const logFileSize = fs.statSync(logPath).size + const logFileSizeLimit = + forceNumber(this.ctx.getConfig>("settings.logFileSizeLimit") || 10) * 1024 * 1024 // 10 MB default + return { + isLarge: logFileSize > logFileSizeLimit, + logFileSize, + logFileSizeLimit, + } + } + return { + isLarge: false, + } + } + + private recreateLogFile(logPath: string): void { + if (fs.existsSync(logPath)) { + fs.unlinkSync(logPath) + fs.createFileSync(logPath) + } + } + + private handleWriteLog(logPath: string, type: string, ...msg: ILogArgvTypeWithError[]): void { + try { + if (this.checkLogLevel(type, this.logLevel)) { + let log = `${dayjs().format("YYYY-MM-DD HH:mm:ss")} [PicGo ${type.toUpperCase()}] ` + msg.forEach((item: ILogArgvTypeWithError) => { + if (item instanceof Error && type === "error") { + log += `\n------Error Stack Begin------\n${util.format(item?.stack)}\n-------Error Stack End------- ` + } else { + if (typeof item === "object") { + item = JSON.stringify(item, null, 2) + } + log += `${item as string} ` + } + }) + log += "\n" + // A synchronized approach to avoid log msg sequence errors + fs.appendFileSync(logPath, log) + } + } catch (e) { + console.error("[PicGo Error] on writing log file", e) + } + } + + private checkLogLevel(type: string, level: undefined | string | string[]): boolean { + if (level === undefined || level === "all") { + return true + } + if (Array.isArray(level)) { + return level.some((item: string) => item === type || item === "all") + } else { + return type === level + } + } + + success(...msg: ILogArgvType[]): void { + return this.handleLog(ILogType.success, ...msg) + } + + info(...msg: ILogArgvType[]): void { + return this.handleLog(ILogType.info, ...msg) + } + + error(...msg: ILogArgvTypeWithError[]): void { + return this.handleLog(ILogType.error, ...msg) + } + + warn(...msg: ILogArgvType[]): void { + return this.handleLog(ILogType.warn, ...msg) + } + + debug(...msg: ILogArgvType[]): void { + if (isDev()) { + this.handleLog(ILogType.info, ...msg) + } + } +} + +export default Logger diff --git a/libs/zhi-picgo-core/src/lib/PluginHandler.ts b/libs/zhi-picgo-core/src/lib/PluginHandler.ts new file mode 100644 index 00000000..e3b2e15d --- /dev/null +++ b/libs/zhi-picgo-core/src/lib/PluginHandler.ts @@ -0,0 +1,319 @@ +import spawn from "cross-spawn" +import { + IResult, + IProcessEnv, + IPluginProcessResult, + IPluginHandler, + IPluginHandlerOptions, + Undefinable, + IPicGo, + IPluginHandlerResult, +} from "../types" +import { IBuildInEvent } from "../utils/enum" +import { getProcessPluginName, getNormalPluginName } 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 + } + + async install( + plugins: string[], + options?: IPluginHandlerOptions, + env?: IProcessEnv + ): Promise> { + if (!options) { + options = {} + } + 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.success(`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) => item.fullName) + const pkgNameList = processPlugins.map((item) => 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) + console.log("execCommand install result=>", result) + if (!result.code) { + pkgNameList.forEach((pluginName: string) => { + this.ctx.pluginLoader.registerPlugin(pluginName) + }) + this.ctx.log.success(this.ctx.i18n.translate("PLUGIN_HANDLER_PLUGIN_INSTALL_SUCCESS")) + this.ctx.emit("installSuccess", { + title: this.ctx.i18n.translate("PLUGIN_HANDLER_PLUGIN_INSTALL_SUCCESS"), + body: [...pkgNameList, ...installedPlugins], + }) + const res: IPluginHandlerResult = { + success: true, + body: [...pkgNameList, ...installedPlugins], + } + return res + } else { + const err = this.ctx.i18n.translate("PLUGIN_HANDLER_PLUGIN_INSTALL_FAILED_REASON", { + code: `${result.code}`, + data: result.data, + }) + this.ctx.log.error(err) + this.ctx.emit("installFailed", { + title: this.ctx.i18n.translate("PLUGIN_HANDLER_PLUGIN_INSTALL_FAILED"), + body: err, + }) + const res: IPluginHandlerResult = { + success: false, + body: err, + } + return res + } + } else if (installedPlugins.length === 0) { + const err = this.ctx.i18n.translate("PLUGIN_HANDLER_PLUGIN_UNINSTALL_FAILED_VALID") + this.ctx.log.error(err) + this.ctx.emit("installFailed", { + title: this.ctx.i18n.translate("PLUGIN_HANDLER_PLUGIN_INSTALL_FAILED"), + body: err, + }) + const res: IPluginHandlerResult = { + success: false, + body: err, + } + return res + } else { + this.ctx.log.success(this.ctx.i18n.translate("PLUGIN_HANDLER_PLUGIN_INSTALL_SUCCESS")) + this.ctx.emit("installSuccess", { + title: this.ctx.i18n.translate("PLUGIN_HANDLER_PLUGIN_INSTALL_SUCCESS"), + body: [...pkgNameList, ...installedPlugins], + }) + const res: IPluginHandlerResult = { + success: true, + body: [...pkgNameList, ...installedPlugins], + } + return res + } + } + + async uninstall( + plugins: string[], + options?: IPluginHandlerOptions, + env?: IProcessEnv + ): Promise> { + if (!options) { + options = {} + } + const processPlugins = plugins + .map((item: string) => handlePluginNameProcess(this.ctx, item)) + .filter((item) => item.success) + const pkgNameList = processPlugins.map((item) => item.pkgName) + if (pkgNameList.length > 0) { + // uninstall plugins must use pkgNameList: + // npm uninstall will use the package.json's name + const result = await this.execCommand("uninstall", pkgNameList, this.ctx.baseDir, options, env) + console.log("execCommand uninstall result=>", result) + if (!result.code) { + pkgNameList.forEach((pluginName: string) => { + this.ctx.pluginLoader.unregisterPlugin(pluginName) + }) + this.ctx.log.success(this.ctx.i18n.translate("PLUGIN_HANDLER_PLUGIN_UNINSTALL_SUCCESS")) + this.ctx.emit("uninstallSuccess", { + title: this.ctx.i18n.translate("PLUGIN_HANDLER_PLUGIN_UNINSTALL_SUCCESS"), + body: pkgNameList, + }) + const res: IPluginHandlerResult = { + success: true, + body: pkgNameList, + } + return res + } else { + const err = this.ctx.i18n.translate("PLUGIN_HANDLER_PLUGIN_UNINSTALL_FAILED_REASON", { + code: `${result.code}`, + data: result.data, + }) + this.ctx.log.error(err) + this.ctx.emit("uninstallFailed", { + title: this.ctx.i18n.translate("PLUGIN_HANDLER_PLUGIN_UNINSTALL_FAILED"), + body: err, + }) + const res: IPluginHandlerResult = { + success: false, + body: err, + } + return res + } + } else { + const err = this.ctx.i18n.translate("PLUGIN_HANDLER_PLUGIN_UNINSTALL_FAILED_VALID") + this.ctx.log.error(err) + this.ctx.emit("uninstallFailed", { + title: this.ctx.i18n.translate("PLUGIN_HANDLER_PLUGIN_UNINSTALL_FAILED"), + body: err, + }) + const res: IPluginHandlerResult = { + success: false, + body: err, + } + return res + } + } + + async update( + plugins: string[], + options?: IPluginHandlerOptions, + env?: IProcessEnv + ): Promise> { + if (!options) { + options = {} + } + const processPlugins = plugins + .map((item: string) => handlePluginNameProcess(this.ctx, item)) + .filter((item) => item.success) + const pkgNameList = processPlugins.map((item) => item.pkgName) + if (pkgNameList.length > 0) { + // update plugins must use pkgNameList: + // npm update will use the package.json's name + const result = await this.execCommand("update", pkgNameList, this.ctx.baseDir, options, env) + console.log("execCommand update result=>", result) + if (!result.code) { + this.ctx.log.success(this.ctx.i18n.translate("PLUGIN_HANDLER_PLUGIN_UPDATE_SUCCESS")) + this.ctx.emit("updateSuccess", { + title: this.ctx.i18n.translate("PLUGIN_HANDLER_PLUGIN_UPDATE_SUCCESS"), + body: pkgNameList, + }) + const res: IPluginHandlerResult = { + success: true, + body: pkgNameList, + } + return res + } else { + const err = this.ctx.i18n.translate("PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED_REASON", { + code: `${result.code}`, + data: result.data, + }) + this.ctx.log.error(err) + this.ctx.emit("updateFailed", { + title: this.ctx.i18n.translate("PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED"), + body: err, + }) + const res: IPluginHandlerResult = { + success: false, + body: err, + } + return res + } + } else { + const err = this.ctx.i18n.translate("PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED_VALID") + this.ctx.log.error(err) + this.ctx.emit("updateFailed", { + title: this.ctx.i18n.translate("PLUGIN_HANDLER_PLUGIN_UPDATE_FAILED"), + body: err, + }) + const res: IPluginHandlerResult = { + success: false, + body: err, + } + return res + } + } + + private async execCommand( + cmd: string, + modules: string[], + where: string, + options: IPluginHandlerOptions = {}, + env: IProcessEnv = {} + ): Promise { + // options first + const registry = options.registry || this.ctx.getConfig>("settings.registry") + const proxy = options.proxy || this.ctx.getConfig>("settings.proxy") + return await new Promise((resolve: any): void => { + let args = [cmd].concat(modules).concat("--color=always").concat("--save") + if (registry) { + args = args.concat(`--registry=${registry}`) + } + if (proxy) { + args = args.concat(`--proxy=${proxy}`) + } + try { + const npmOptions = { cwd: where, env: Object.assign({}, process.env, env) } + console.log("Start run npm, args=>", args) + console.log("Start run npm, npmOptions=>", npmOptions) + const npm = spawn("npm", args, npmOptions) + + let output = "" + npm.stdout + ?.on("data", (data: string) => { + output += data + }) + .pipe(process.stdout) + + npm.stderr + ?.on("data", (data: string) => { + output += data + }) + .pipe(process.stderr) + + npm.on("close", (code: number) => { + if (!code) { + resolve({ code: 0, data: output }) + } else { + resolve({ code: code, data: output }) + } + }) + // for users who haven't installed node.js + npm.on("error", (err: Error) => { + this.ctx.log.error(err) + this.ctx.log.error("NPM is not installed") + this.ctx.emit(IBuildInEvent.FAILED, "NPM is not installed") + }) + } catch (e) { + this.ctx.log.error(e as Error) + this.ctx.emit(IBuildInEvent.FAILED, e) + } + }) + } +} + +/** + * 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, + } +} + +export default PluginHandler diff --git a/libs/zhi-picgo-core/src/lib/PluginLoader.ts b/libs/zhi-picgo-core/src/lib/PluginLoader.ts new file mode 100644 index 00000000..54346865 --- /dev/null +++ b/libs/zhi-picgo-core/src/lib/PluginLoader.ts @@ -0,0 +1,154 @@ +import fs from "fs-extra" +import path from "path" +import resolve from "resolve" +import { IBuildInEvent } from "../utils/enum" +import { IPicGo, IPicGoPlugin, IPluginLoader, IPicGoPluginInterface } from "../types/index" +import { setCurrentPluginName } from "./LifecyclePlugins" + +/** + * Local plugin loader, file system is required + */ +export class PluginLoader implements IPluginLoader { + private readonly ctx: IPicGo + private list: string[] = [] + private readonly fullList: Set = new Set() + private readonly pluginMap: Map = new Map() + constructor(ctx: IPicGo) { + this.ctx = ctx + this.init() + } + + private init(): void { + const packagePath = path.join(this.ctx.baseDir, "package.json") + if (!fs.existsSync(packagePath)) { + const pkg = { + name: "picgo-plugins", + description: "picgo-plugins", + repository: "https://github.com/PicGo/PicGo-Core", + license: "MIT", + } + fs.writeFileSync(packagePath, JSON.stringify(pkg), "utf8") + } + } + + // get plugin entry + private resolvePlugin(ctx: IPicGo, name: string): string { + try { + return resolve.sync(name, { basedir: ctx.baseDir }) + } catch (err) { + return path.join(ctx.baseDir, "node_modules", name) + } + } + + // load all third party plugin + load(): boolean { + const packagePath = path.join(this.ctx.baseDir, "package.json") + const pluginDir = path.join(this.ctx.baseDir, "node_modules/") + // Thanks to hexo -> https://github.com/hexojs/hexo/blob/master/lib/hexo/load_plugins.js + if (!fs.existsSync(pluginDir)) { + return false + } + const json = fs.readJSONSync(packagePath) + const deps = Object.keys(json.dependencies || {}) + const devDeps = Object.keys(json.devDependencies || {}) + const modules = deps.concat(devDeps).filter((name: string) => { + if (!/^picgo-plugin-|^@[^/]+\/picgo-plugin-/.test(name)) return false + const path = this.resolvePlugin(this.ctx, name) + return fs.existsSync(path) + }) + for (const module of modules) { + this.registerPlugin(module) + } + return true + } + + registerPlugin(name: string, plugin?: IPicGoPlugin): void { + if (!name || typeof name !== "string") { + this.ctx.log.warn("Please provide valid plugin") + return + } + console.log("Start registering plugin " + name + " ...") + this.fullList.add(name) + try { + // register local plugin + if (!plugin) { + if ( + this.ctx.getConfig(`picgoPlugins.${name}`) === true || + this.ctx.getConfig(`picgoPlugins.${name}`) === undefined + ) { + this.list.push(name) + setCurrentPluginName(name) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.getPlugin(name)!.register(this.ctx) + const plugin = `picgoPlugins[${name}]` + this.ctx.saveConfig({ + [plugin]: true, + }) + } + } else { + // register provided plugin + // && won't write config to files + this.list.push(name) + setCurrentPluginName(name) + const pluginInterface = plugin(this.ctx) + this.pluginMap.set(name, pluginInterface) + pluginInterface.register(this.ctx) + } + } catch (e) { + this.pluginMap.delete(name) + this.list = this.list.filter((item: string) => item !== name) + this.fullList.delete(name) + this.ctx.log.error(e as Error) + this.ctx.emit(IBuildInEvent.NOTIFICATION, { + title: `Plugin ${name} Load Error`, + body: e, + }) + } + } + + unregisterPlugin(name: string): void { + this.list = this.list.filter((item: string) => item !== name) + this.fullList.delete(name) + this.pluginMap.delete(name) + setCurrentPluginName(name) + this.ctx.helper.uploader.unregister(name) + this.ctx.helper.transformer.unregister(name) + this.ctx.helper.beforeTransformPlugins.unregister(name) + this.ctx.helper.beforeUploadPlugins.unregister(name) + this.ctx.helper.afterUploadPlugins.unregister(name) + this.ctx.cmd.unregister(name) + this.ctx.removeConfig("picgoPlugins", name) + } + + // get plugin by name + getPlugin(name: string): IPicGoPluginInterface | undefined { + if (this.pluginMap.has(name)) { + return this.pluginMap.get(name) + } + const pluginDir = path.join(this.ctx.baseDir, "node_modules/") + console.log("Start requiring plugin lib, pluginDir", pluginDir) + console.log("Start requiring plugin lib, name", name) + const plugin = require(pluginDir + name)(this.ctx) + this.pluginMap.set(name, plugin) + return plugin + } + + /** + * Get the list of enabled plugins + */ + getList(): string[] { + return this.list + } + + hasPlugin(name: string): boolean { + return this.fullList.has(name) + } + + /** + * Get the full list of plugins, whether it is enabled or not + */ + getFullList(): string[] { + return [...this.fullList] + } +} +export default PluginLoader diff --git a/libs/zhi-picgo-core/src/lib/Request.ts b/libs/zhi-picgo-core/src/lib/Request.ts new file mode 100644 index 00000000..812e3a7b --- /dev/null +++ b/libs/zhi-picgo-core/src/lib/Request.ts @@ -0,0 +1,257 @@ +/* eslint-disable @typescript-eslint/explicit-function-return-type */ +/* eslint-disable @typescript-eslint/promise-function-async */ +import axios, { AxiosRequestConfig, AxiosResponse } from "axios" +import { + IPicGo, + Undefinable, + IConfigChangePayload, + IConfig, + IRequestConfig, + IOldReqOptions, + IResponse, + IFullResponse, + IRequest, +} from "../types" +import { IBusEvent } from "../utils/enum" +import { eventBus } from "../utils/eventBus" +import { URL } from "url" +import FormData from "form-data" +import https from "https" +import tunnel from "tunnel" +const httpsAgent = new https.Agent({ + maxVersion: "TLSv1.2", + minVersion: "TLSv1.2", +}) + +// thanks for https://github.dev/request/request/blob/master/index.js +function appendFormData(form: FormData, key: string, data: any): void { + if (typeof data === "object" && "value" in data && "options" in data) { + form.append(key, data.value, data.options) + } else { + form.append(key, data) + } +} + +function requestInterceptor(options: IOldReqOptions | AxiosRequestConfig): AxiosRequestConfig & { + __isOldOptions?: boolean +} { + let __isOldOptions = false + const opt: AxiosRequestConfig & { + __isOldOptions?: boolean + } = { + ...options, + url: (options.url as string) || "", + headers: options.headers || {}, + } + // user request config proxy + if (options.proxy) { + let proxyOptions = options.proxy + if (typeof proxyOptions === "string") { + try { + proxyOptions = new URL(options.proxy) + } catch (e) { + proxyOptions = false + opt.proxy = false + console.error(e) + } + __isOldOptions = true + } + if (proxyOptions) { + if (options.url?.startsWith("https://")) { + opt.proxy = false + opt.httpsAgent = tunnel.httpsOverHttp({ + proxy: { + host: proxyOptions?.hostname, + port: parseInt(proxyOptions?.port, 10), + }, + }) + } else { + opt.proxy = { + host: proxyOptions.hostname, + port: parseInt(proxyOptions.port, 10), + protocol: "http", + } + } + } + } + if ("formData" in options) { + const form = new FormData() + for (const key in options.formData) { + const data = options.formData[key] + appendFormData(form, key, data) + } + opt.data = form + opt.headers = Object.assign(opt.headers || {}, form.getHeaders()) + __isOldOptions = true + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + delete opt.formData + } + if ("body" in options) { + opt.data = options.body + __isOldOptions = true + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + delete opt.body + } + if ("qs" in options) { + opt.params = options.qs + __isOldOptions = true + } + opt.__isOldOptions = __isOldOptions + return opt +} + +function responseInterceptor(response: AxiosResponse): IFullResponse { + return { + ...response, + statusCode: response.status, + body: response.data, + } +} + +function responseErrorHandler(error: any) { + // if (error.response) { + // // The request was made and the server responded with a status code + // // that falls out of the range of 2xx + // return Promise.reject(erro) + // } else if (error.request) { + // // The request was made but no response was received + // // `error.request` is an instance of XMLHttpRequest in the browser and an instance of + // // http.ClientRequest in node.js + // return Promise.reject(error.request) + // } else { + // // Something happened in setting up the request that triggered an Error + // return Promise.reject(error.message) + // } + const errorObj = { + method: error?.config?.method?.toUpperCase() || "", + url: error?.config?.url || "", + statusCode: error?.response?.status || 0, + message: error?.message || "", + stack: error?.stack || {}, + response: { + status: error?.response?.status || 0, + statusCode: error?.response?.status || 0, + body: error?.response?.data || "", + }, + } + return Promise.reject(errorObj) +} + +export class Request implements IRequest { + private readonly ctx: IPicGo + private proxy: Undefinable = "" + options: AxiosRequestConfig = {} + constructor(ctx: IPicGo) { + this.ctx = ctx + this.init() + eventBus.on(IBusEvent.CONFIG_CHANGE, (data: IConfigChangePayload) => { + switch (data.configName) { + case "picBed": + if ((data.value as IConfig["picBed"])?.proxy) { + this.proxy = (data.value as IConfig["picBed"]).proxy + } + break + case "picBed.proxy": + this.proxy = data.value as string + break + } + }) + } + + private init(): void { + const proxy = this.ctx.getConfig>("picBed.proxy") + if (proxy) { + this.proxy = proxy + } + } + + private handleProxy(): AxiosRequestConfig["proxy"] | false { + if (this.proxy) { + try { + const proxyOptions = new URL(this.proxy) + return { + host: proxyOptions.hostname, + port: parseInt(proxyOptions.port || "0", 10), + protocol: proxyOptions.protocol, + } + } catch (e) { + console.error(e) + } + } + return false + } + + // #64 dynamic get proxy value + request< + T, + U extends IRequestConfig extends IOldReqOptions + ? IOldReqOptions + : IRequestConfig extends AxiosRequestConfig + ? AxiosRequestConfig + : never + >(options: U): Promise> { + this.options.proxy = this.handleProxy() + this.options.headers = options.headers || {} + this.options.maxBodyLength = Infinity + this.options.maxContentLength = Infinity + if (this.options.proxy && options.url?.startsWith("https://")) { + this.options.httpsAgent = tunnel.httpsOverHttp({ + proxy: { + host: this.options.proxy.host, + port: this.options.proxy.port, + }, + }) + this.options.proxy = false + } else { + this.options.httpsAgent = httpsAgent + } + // !NOTICE this.options !== options + // this.options is the default options + const instance = axios.create(this.options) + instance.interceptors.response.use(responseInterceptor, responseErrorHandler) + + // compatible with old request options to new options + const opt = requestInterceptor(options) + + instance.interceptors.request.use(function (obj) { + // handle Content-Type + let contentType = "" + if (obj?.headers?.contentType) { + contentType = obj.headers.contentType as string + delete obj.headers.contentType + } else if (obj?.headers?.ContentType) { + contentType = obj.headers.ContentType as string + delete obj.headers.ContentType + } else if (obj?.headers?.["content-type"]) { + contentType = obj.headers["content-type"] as string + delete obj.headers["content-type"] + } + if (contentType !== "" && obj.headers) { + obj.headers["Content-Type"] = contentType + } + return obj + }) + if ("resolveWithFullResponse" in options && options.resolveWithFullResponse) { + return instance.request(opt) + } else { + return instance.request(opt).then((res) => { + // use old request option format + if (opt.__isOldOptions) { + if ("json" in options) { + if (options.json) { + return res.data + } + } else { + return JSON.stringify(res.data) + } + } else { + return res.data + } + }) as Promise> + } + } +} + +export default Request diff --git a/libs/zhi-picgo-core/src/libs.d.ts b/libs/zhi-picgo-core/src/libs.d.ts new file mode 100644 index 00000000..5066d4e8 --- /dev/null +++ b/libs/zhi-picgo-core/src/libs.d.ts @@ -0,0 +1,9 @@ +declare module 'download-git-repo' { + function download (...args: any[]): any + export = download +} +declare module 'lodash-id' { + // eslint-disable-next-line no-var + var lodashId: any + export = lodashId +} diff --git a/libs/zhi-picgo-core/src/plugins/commander/config.ts b/libs/zhi-picgo-core/src/plugins/commander/config.ts new file mode 100644 index 00000000..133afb47 --- /dev/null +++ b/libs/zhi-picgo-core/src/plugins/commander/config.ts @@ -0,0 +1,11 @@ +import { IPicGo, IPlugin } from "../../types" + +const config: IPlugin = { + handle: (ctx: IPicGo) => { + const cmd = ctx.cmd + cmd.program.option("-c, --config ", "set config path") + // will handle in `bin/picgo` + }, +} + +export default config diff --git a/libs/zhi-picgo-core/src/plugins/commander/i18n.ts b/libs/zhi-picgo-core/src/plugins/commander/i18n.ts new file mode 100644 index 00000000..781483c9 --- /dev/null +++ b/libs/zhi-picgo-core/src/plugins/commander/i18n.ts @@ -0,0 +1,36 @@ +import { IPlugin, IPicGo, IStringKeyMap } from "../../types" + +const i18n: IPlugin = { + handle: (ctx: IPicGo) => { + const cmd = ctx.cmd + cmd.program + .command("i18n") + .arguments("[lang]") + .description("change picgo language") + .action(async (lang = "") => { + const list = ctx.i18n.getLanguageList() + if (!lang) { + const prompts = [ + { + type: "list", + name: "i18n", + choices: list, + message: "Choose a language", + default: ctx.getConfig("settings.language") || "zh-CN", + }, + ] + const answer = await ctx.cmd.inquirer.prompt>(prompts) + ctx.i18n.setLanguage(answer.i18n) + ctx.log.success(`Language set to ${answer.i18n}`) + return + } + if (!list.includes(lang)) { + return ctx.log.warn("No such language") + } + ctx.i18n.setLanguage(lang) + ctx.log.success(`Language set to ${lang}`) + }) + }, +} + +export default i18n diff --git a/libs/zhi-picgo-core/src/plugins/commander/index.ts b/libs/zhi-picgo-core/src/plugins/commander/index.ts new file mode 100644 index 00000000..b0c0101c --- /dev/null +++ b/libs/zhi-picgo-core/src/plugins/commander/index.ts @@ -0,0 +1,20 @@ +import pluginHandler from "./pluginHandler" +import config from "./config" +import upload from "./upload" +import setting from "./setting" +import use from "./use" +import proxy from "./proxy" +import init from "./init" +import i18n from "./i18n" +import { IPicGo } from "../../types" + +export default (ctx: IPicGo): void => { + ctx.cmd.register("pluginHandler", pluginHandler) + ctx.cmd.register("config", config) + ctx.cmd.register("setting", setting) + ctx.cmd.register("upload", upload) + ctx.cmd.register("use", use) + ctx.cmd.register("proxy", proxy) + ctx.cmd.register("init", init) + ctx.cmd.register("i18n", i18n) +} diff --git a/libs/zhi-picgo-core/src/plugins/commander/init.ts b/libs/zhi-picgo-core/src/plugins/commander/init.ts new file mode 100644 index 00000000..df9792dc --- /dev/null +++ b/libs/zhi-picgo-core/src/plugins/commander/init.ts @@ -0,0 +1,129 @@ +import chalk from "chalk" +import path from "path" +import fs from "fs-extra" +import { generate } from "../../utils/initUtils" +import { homedir } from "os" +import download from "download-git-repo" +import { IOptions, IPlugin, IPicGo } from "../../types" +import rm from "rimraf" + +const run = (ctx: IPicGo, options: IOptions): void => { + // const name = options.inPlace ? path.relative('../', process.cwd()) : options.project + if (options.offline) { + // offline mode + if (fs.existsSync(options.template)) { + generate(ctx, options).catch((e) => { + ctx.log.error(e) + }) + } else { + ctx.log.error(`Local template ${options.template} not found`) + } + } else { + // online mode + options.template = !options.hasSlash + ? "PicGo/picgo-template-" + options.template // official template + : options.template + downloadAndGenerate(ctx, options) + } +} + +/** + * download template & generate + * @param { PicGo } ctx + * @param { IOptions } options + */ +const downloadAndGenerate = (ctx: IPicGo, options: IOptions): void => { + if (fs.existsSync(options.tmp)) { + rm.sync(options.tmp) + } + ctx.log.info("Template files are downloading...") + download(options.template, options.tmp, { clone: options.clone }, (err: Error) => { + if (err) { + return ctx.log.error(err) + } + ctx.log.success("Template files are downloaded!") + generate(ctx, options).catch((e) => { + ctx.log.error(e) + }) + }) +} + +const init: IPlugin = { + handle: async (ctx: IPicGo) => { + const cmd = ctx.cmd + cmd.program + .command("init") + .arguments("