Merge branch 'template'
# Conflicts: # README.md # addon/locale/en-US/preferences.ftl # package.json # src/hooks.ts # src/modules/examples.ts # update.json
This commit is contained in:
commit
5cc74c4986
|
|
@ -1,41 +0,0 @@
|
||||||
{
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es2021": true
|
|
||||||
},
|
|
||||||
"root": true,
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended",
|
|
||||||
"prettier"
|
|
||||||
],
|
|
||||||
"overrides": [],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": "latest",
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"plugins": ["@typescript-eslint"],
|
|
||||||
"rules": {
|
|
||||||
"@typescript-eslint/ban-ts-comment": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
"ts-expect-error": "allow-with-description",
|
|
||||||
"ts-ignore": "allow-with-description",
|
|
||||||
"ts-nocheck": "allow-with-description",
|
|
||||||
"ts-check": "allow-with-description"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
|
||||||
"@typescript-eslint/no-explicit-any": ["off", { "ignoreRestArgs": true }],
|
|
||||||
"@typescript-eslint/no-non-null-assertion": "off"
|
|
||||||
},
|
|
||||||
"ignorePatterns": [
|
|
||||||
"**/build/**",
|
|
||||||
"**/dist/**",
|
|
||||||
"**/node_modules/**",
|
|
||||||
"**/scripts/**",
|
|
||||||
"**/*.js",
|
|
||||||
"**/*.bak"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:recommended",
|
||||||
|
":semanticPrefixChore",
|
||||||
|
":prHourlyLimitNone",
|
||||||
|
":prConcurrentLimitNone",
|
||||||
|
":enableVulnerabilityAlerts",
|
||||||
|
":dependencyDashboard",
|
||||||
|
"schedule:weekends"
|
||||||
|
],
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"matchPackageNames": ["zotero-plugin-toolkit", "zotero-types"],
|
||||||
|
"automerge": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"git-submodules": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v**
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GitHub_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Install deps
|
||||||
|
run: npm install
|
||||||
|
|
||||||
|
- name: Release to GitHub
|
||||||
|
run: |
|
||||||
|
npm run release -- --no-increment --no-git --github.release --ci --VV
|
||||||
|
sleep 1s
|
||||||
|
|
||||||
|
- name: Notify release
|
||||||
|
uses: apexskier/github-release-commenter@v1
|
||||||
|
continue-on-error: true
|
||||||
|
with:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
comment-template: |
|
||||||
|
:rocket: _This ticket has been resolved in {release_tag}. See {release_link} for release notes._
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
**/build
|
build
|
||||||
|
logs
|
||||||
node_modules
|
node_modules
|
||||||
package-lock.json
|
package-lock.json
|
||||||
zotero-cmd.json
|
pnpm-lock.yaml
|
||||||
|
yarn.lock
|
||||||
|
zotero-cmd.json
|
||||||
|
.DS_Store
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
build
|
||||||
|
logs
|
||||||
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
pnpm-lock.yaml
|
||||||
|
# zotero-cmd.json
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"tabWidth": 2
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"npm": {
|
|
||||||
"publish": false
|
|
||||||
},
|
|
||||||
"github": {
|
|
||||||
"release": true,
|
|
||||||
"assets": ["build/*.xpi"]
|
|
||||||
},
|
|
||||||
"hooks": {
|
|
||||||
"before:init": "npm run lint",
|
|
||||||
"after:bump": "npm run build",
|
|
||||||
"after:release": "echo Successfully released ${name} v${version} to ${repo.repository}."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -7,23 +7,16 @@
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "StartDev",
|
"name": "Start",
|
||||||
"runtimeExecutable": "npm",
|
"runtimeExecutable": "npm",
|
||||||
"runtimeArgs": ["run", "start-watch"]
|
"runtimeArgs": ["run", "start"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "node",
|
"type": "node",
|
||||||
"request": "launch",
|
"request": "launch",
|
||||||
"name": "Restart",
|
"name": "Build",
|
||||||
"runtimeExecutable": "npm",
|
"runtimeExecutable": "npm",
|
||||||
"runtimeArgs": ["run", "restart"]
|
"runtimeArgs": ["run", "build"]
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "node",
|
|
||||||
"request": "launch",
|
|
||||||
"name": "Restart in Prod Mode",
|
|
||||||
"runtimeExecutable": "npm",
|
|
||||||
"runtimeArgs": ["run", "restart-prod"]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ async function startup({ id, version, resourceURI, rootURI }, reason) {
|
||||||
`${rootURI}/chrome/content/scripts/__addonRef__.js`,
|
`${rootURI}/chrome/content/scripts/__addonRef__.js`,
|
||||||
ctx,
|
ctx,
|
||||||
);
|
);
|
||||||
|
Zotero.__addonInstance__.hooks.onStartup();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onMainWindowLoad({ window }, reason) {
|
async function onMainWindowLoad({ window }, reason) {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
"applications": {
|
"applications": {
|
||||||
"zotero": {
|
"zotero": {
|
||||||
"id": "__addonID__",
|
"id": "__addonID__",
|
||||||
"update_url": "__updaterdf__",
|
"update_url": "__updateURL__",
|
||||||
"strict_min_version": "6.999",
|
"strict_min_version": "6.999",
|
||||||
"strict_max_version": "7.0.*"
|
"strict_max_version": "7.0.*"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,420 @@
|
||||||
|
# Zotero Plugin Template
|
||||||
|
|
||||||
|
[](https://www.zotero.org)
|
||||||
|
[](https://github.com/windingwind/zotero-plugin-template)
|
||||||
|
|
||||||
|
这是 [Zotero](https://www.zotero.org/) 的插件模板.
|
||||||
|
|
||||||
|
[English](../README.md) | [简体中文](./README-zhCN.md)
|
||||||
|
|
||||||
|
📖 [插件开发文档](https://zotero.yuque.com/books/share/8d230829-6004-4934-b4c6-685a7001bfa0/vec88d) (中文版,已过时)
|
||||||
|
|
||||||
|
[📖 Zotero 7 插件开发文档](https://www.zotero.org/support/dev/zotero_7_for_developers)
|
||||||
|
|
||||||
|
🛠️ [Zotero 插件工具包](https://github.com/windingwind/zotero-plugin-toolkit) | [API 文档](https://github.com/windingwind/zotero-plugin-toolkit/blob/master/docs/zotero-plugin-toolkit.md)
|
||||||
|
|
||||||
|
ℹ️ [Zotero 类型定义](https://github.com/windingwind/zotero-types)
|
||||||
|
|
||||||
|
📜 [Zotero 源代码](https://github.com/zotero/zotero)
|
||||||
|
|
||||||
|
📌 [Zotero 插件模板](https://github.com/windingwind/zotero-plugin-template) (即本仓库)
|
||||||
|
|
||||||
|
> [!tip]
|
||||||
|
> 👁 Watch 本仓库,以及时收到修复或更新的通知.
|
||||||
|
|
||||||
|
## 使用此模板构建的插件
|
||||||
|
|
||||||
|
[](https://github.com/windingwind/zotero-better-notes)
|
||||||
|
[](https://github.com/windingwind/zotero-pdf-preview)
|
||||||
|
[](https://github.com/windingwind/zotero-pdf-translate)
|
||||||
|
[](https://github.com/windingwind/zotero-tag)
|
||||||
|
[](https://github.com/iShareStuff/ZoteroTheme)
|
||||||
|
[](https://github.com/MuiseDestiny/zotero-reference)
|
||||||
|
[](https://github.com/MuiseDestiny/zotero-citation)
|
||||||
|
[](https://github.com/MuiseDestiny/ZoteroStyle)
|
||||||
|
[](https://github.com/volatile-static/Chartero)
|
||||||
|
[](https://github.com/l0o0/tara)
|
||||||
|
[](https://github.com/redleafnew/delitemwithatt)
|
||||||
|
[](https://github.com/redleafnew/zotero-updateifsE)
|
||||||
|
[](https://github.com/northword/zotero-format-metadata)
|
||||||
|
[](https://github.com/inciteful-xyz/inciteful-zotero-plugin)
|
||||||
|
[](https://github.com/MuiseDestiny/zotero-gpt)
|
||||||
|
[](https://github.com/zoushucai/zotero-journalabbr)
|
||||||
|
[](https://github.com/MuiseDestiny/zotero-figure)
|
||||||
|
[](https://github.com/l0o0/jasminum)
|
||||||
|
[](https://github.com/lifan0127/ai-research-assistant)
|
||||||
|
|
||||||
|
[](https://github.com/daeh/zotero-markdb-connect)
|
||||||
|
|
||||||
|
如果你正在使用此库,我建议你将这个标志 ([](https://github.com/windingwind/zotero-plugin-template)) 放在 README 文件中:
|
||||||
|
|
||||||
|
```md
|
||||||
|
[](https://github.com/windingwind/zotero-plugin-template)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features 特性
|
||||||
|
|
||||||
|
- 事件驱动、函数式编程的可扩展框架;
|
||||||
|
- 简单易用,开箱即用;
|
||||||
|
- ⭐[新特性!]自动热重载!每当修改源码时,都会自动编译并重新加载插件;[详情请跳转→](#自动热重载)
|
||||||
|
- `src/modules/examples.ts` 中有丰富的示例,涵盖了插件中常用的大部分API (使用的插件工具包 zotero-plugin-toolkit,仓库地址 https://github.com/windingwind/zotero-plugin-toolkit);
|
||||||
|
- TypeScript 支持:
|
||||||
|
- 为使用 JavaScript 编写的Zotero源码提供全面的类型定义支持 (使用类型定义包 zotero-types,仓库地址 https://github.com/windingwind/zotero-types);
|
||||||
|
- 全局变量和环境设置;
|
||||||
|
- 插件开发/构建/发布工作流:
|
||||||
|
- 自动生成/更新插件id和版本、更新配置和设置环境变量 (`development`/`production`);
|
||||||
|
- 自动在 Zotero 中构建和重新加载代码;
|
||||||
|
- 自动发布到GitHub (使用[release-it](https://github.com/release-it/release-it));
|
||||||
|
- 集成Prettier和ES Lint;
|
||||||
|
|
||||||
|
> [!warning]
|
||||||
|
> Zotero本地化已升级(`dtd` 已弃用,我们将不再使用 `.properties`). 主分支将只支持 Zotero 7.0.0-beta.12 或更高版本. 如果需要支持 Zotero 6,你可能需要同时使用`dtd`、`properties` 和`ftl`. 请参考此库的 `zotero6-bootstrap` 分支.
|
||||||
|
|
||||||
|
## Examples 示例
|
||||||
|
|
||||||
|
此库提供了 [zotero-plugin-toolkit](https://github.com/windingwind/zotero-plugin-toolkit) 中API的示例.
|
||||||
|
|
||||||
|
在 `src/examples.ts` 中搜索`@example` 查看示例. 这些示例在 `src/hooks.ts` 中调用演示.
|
||||||
|
|
||||||
|
### 基本示例(Basic Examples)
|
||||||
|
|
||||||
|
- registerNotifier
|
||||||
|
- registerPrefs, unregisterPrefs
|
||||||
|
|
||||||
|
### 快捷键示例(Shortcut Keys Examples)
|
||||||
|
|
||||||
|
- registerShortcuts
|
||||||
|
- exampleShortcutLargerCallback
|
||||||
|
- exampleShortcutSmallerCallback
|
||||||
|
- exampleShortcutConflictionCallback
|
||||||
|
|
||||||
|
### UI示例(UI Examples)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- registerStyleSheet(the official make-it-red example)
|
||||||
|
- registerRightClickMenuItem
|
||||||
|
- registerRightClickMenuPopup
|
||||||
|
- registerWindowMenuWithSeprator
|
||||||
|
- registerExtraColumn
|
||||||
|
- registerExtraColumnWithCustomCell
|
||||||
|
- registerCustomItemBoxRow
|
||||||
|
- registerLibraryTabPanel
|
||||||
|
- registerReaderTabPanel
|
||||||
|
|
||||||
|
### 首选项面板示例(Preference Pane Examples)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- Preferences bindings
|
||||||
|
- UI Events
|
||||||
|
- Table
|
||||||
|
- Locale
|
||||||
|
|
||||||
|
详情参见 [`src/modules/preferenceScript.ts`](./src/modules/preferenceScript.ts)
|
||||||
|
|
||||||
|
### 帮助示例(HelperExamples)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- dialogExample
|
||||||
|
- clipboardExample
|
||||||
|
- filePickerExample
|
||||||
|
- progressWindowExample
|
||||||
|
- vtableExample(See Preference Pane Examples)
|
||||||
|
|
||||||
|
### 指令行示例(PromptExamples)
|
||||||
|
|
||||||
|
Obsidian风格的指令输入模块,它通过接受文本来运行插件,并在弹出窗口中显示可选项.
|
||||||
|
|
||||||
|
使用 `Shift+P` 激活.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- registerAlertPromptExample
|
||||||
|
|
||||||
|
## Quick Start Guide 快速入门指南
|
||||||
|
|
||||||
|
### 0 前置要求(Requirement)
|
||||||
|
|
||||||
|
1. 安装测试版 Zotero:https://www.zotero.org/support/beta_builds
|
||||||
|
2. 安装 Node.js(https://nodejs.org/en/)和 Git(https://git-scm.com/)
|
||||||
|
|
||||||
|
> [!note]
|
||||||
|
> 本指南假定你已经对 Zotero 插件的基本结构和工作原理有初步的了解. 如果你还不了解,请先参考官方文档(https://www.zotero.org/support/dev/zotero_7_for_developers)和官方插件样例 Make It Red(仓库地址 https://github.com/zotero/make-it-red).
|
||||||
|
|
||||||
|
### 1 创建你的仓库(Create Your Repo)
|
||||||
|
|
||||||
|
1. 点击 `Use this template`;
|
||||||
|
2. 使用 `git clone` 克隆上一步生成的仓库;
|
||||||
|
<details >
|
||||||
|
<summary>💡 从 GitHub Codespace 开始</summary>
|
||||||
|
|
||||||
|
_GitHub CodeSpace_ 使你可以直接开始开发而无需在本地下载代码/IDE/依赖.
|
||||||
|
|
||||||
|
重复下列步骤,仅需三十秒即可开始构建你的第一个插件!
|
||||||
|
|
||||||
|
- 去 [homepage](https://github.com/windingwind/zotero-plugin-template)顶部,点击绿色按钮`Use this template`,点击 `Open in codespace`, 你需要登录你的GitHub账号.
|
||||||
|
- 等待 codespace 加载.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
3. 进入项目文件夹;
|
||||||
|
|
||||||
|
### 2 配置模板和开发环境(Config Template Settings and Enviroment)
|
||||||
|
|
||||||
|
1. 修改 `./package.json` 中的设置,包括:
|
||||||
|
|
||||||
|
```json5
|
||||||
|
{
|
||||||
|
version: "", // to 0.0.0
|
||||||
|
author: "",
|
||||||
|
description: "",
|
||||||
|
homepage: "",
|
||||||
|
config: {
|
||||||
|
addonName: "", // name to be displayed in the plugin manager
|
||||||
|
addonID: "", // ID to avoid conflict. IMPORTANT!
|
||||||
|
addonRef: "", // e.g. Element ID prefix
|
||||||
|
addonInstance: "", // the plugin's root instance: Zotero.${addonInstance}
|
||||||
|
prefsPrefix: "extensions.zotero.${addonRef}", // the prefix of prefs
|
||||||
|
releasePage: "", // URL to releases
|
||||||
|
updateJSON: "", // URL to update.json
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!warning]
|
||||||
|
> 注意设置 addonID 和 addonRef 以避免冲突.
|
||||||
|
|
||||||
|
如果你需要在GitHub以外的地方托管你的 XPI 包,请删除 `releasePage` 并添加 `updateLink`,并将值设置为你的 XPI 下载地址.
|
||||||
|
|
||||||
|
2. 复制 Zotero 启动配置,填入 Zotero 可执行文件路径和 profile 路径.
|
||||||
|
|
||||||
|
> (可选项) 此操作仅需执行一次: 使用 `/path/to/zotero -p` 启动 Zotero,创建一个新的配置文件并用作开发配置文件.
|
||||||
|
> 将配置文件的路径 `profilePath` 放入 `zotero-cmd.json` 中,以指定要使用的配置文件.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cp ./scripts/zotero-cmd-template.json ./scripts/zotero-cmd.json
|
||||||
|
vim ./scripts/zotero-cmd.json
|
||||||
|
```
|
||||||
|
|
||||||
|
3. 运行 `npm install` 以安装相关依赖
|
||||||
|
|
||||||
|
> 如果你使用 `pnpm` 作为包管理器,你需要添加 `public-hoist-pattern[]=*@types/bluebird*` 到`.npmrc`, 详情请查看 zotero-types(https://github.com/windingwind/zotero-types?tab=readme-ov-file#usage)的文档.
|
||||||
|
|
||||||
|
### 3 开始开发(Coding)
|
||||||
|
|
||||||
|
使用 `npm start` 启动开发服务器,它将:
|
||||||
|
|
||||||
|
- 在开发模式下预构建插件
|
||||||
|
- 启动 Zotero ,并让其从 `build/` 中加载插件
|
||||||
|
- 打开开发者工具(devtool)
|
||||||
|
- 监听 `src/**` 和 `addon/**`.
|
||||||
|
- 如果 `src/**` 修改了,运行 esbuild 并且重新加载
|
||||||
|
- 如果 `addon/**` 修改了,(在开发模式下)重新构建插件并且重新加载
|
||||||
|
|
||||||
|
#### 自动热重载
|
||||||
|
|
||||||
|
厌倦了无休止的重启吗?忘掉它,拥抱热加载!
|
||||||
|
|
||||||
|
1. 运行 `npm start`.
|
||||||
|
2. 编码. (是的,就这么简单)
|
||||||
|
|
||||||
|
当检测到 `src` 或 `addon` 中的文件修改时,插件将自动编译并重新加载.
|
||||||
|
|
||||||
|
<details style="text-indent: 2em">
|
||||||
|
<summary>💡 将此功能添加到现有插件的步骤</summary>
|
||||||
|
|
||||||
|
1. 复制 `scripts/**.mjs`
|
||||||
|
2. 复制 `server` 、`build` 和 `stop` 命令到 `package.json`
|
||||||
|
3. 运行 `npm install --save-dev chokidar`
|
||||||
|
4. 结束.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
#### 在 Zotero 中 Debug
|
||||||
|
|
||||||
|
你还可以:
|
||||||
|
|
||||||
|
- 在 Tools->Developer->Run Javascript 中测试代码片段;
|
||||||
|
|
||||||
|
- 使用 `Zotero.debug()` 调试输出. 在 Help->Debug Output Logging->View Output 查看输出;
|
||||||
|
|
||||||
|
- 调试 UI. Zotero 建立在 Firefox XUL 框架之上. 使用 [XUL Explorer](https://udn.realityripple.com/docs/Archive/Mozilla/XUL_Explorer) 等软件调试 XUL UI.
|
||||||
|
|
||||||
|
> XUL 文档: <http://www.devdoc.net/web/developer.mozilla.org/en-US/docs/XUL.html>
|
||||||
|
|
||||||
|
### 4 构建(Build)
|
||||||
|
|
||||||
|
运行 `npm run build` 在生产模式下构建插件,构建的结果位于 `build/` 目录中.
|
||||||
|
|
||||||
|
`scripts/build.mjs` 的运行步骤:
|
||||||
|
|
||||||
|
- 创建/清空 `build/`
|
||||||
|
- 复制 `addon/**` 到 `build/addon/**`
|
||||||
|
- 替换占位符:使用 `replace-in-file` 去替换在 `package.json` 中定义的关键字和配置 (`xhtml`、`.flt` 等)
|
||||||
|
- 准备本地化文件以避免冲突,查看官方文档了解更多(https://www.zotero.org/support/dev/zotero_7_for_developers#avoiding_localization_conflicts)
|
||||||
|
- 重命名`**/*.flt` 为 `**/${addonRef}-*.flt`
|
||||||
|
- 在每个消息前加上 `addonRef-`
|
||||||
|
- 使用 Esbuild 来将 `.ts` 源码构建为 `.js`,从 `src/index.ts` 构建到`./build/addon/chrome/content/scripts`
|
||||||
|
- (仅在生产模式下工作) 压缩 `./build/addon` 目录为 `./build/*.xpi`
|
||||||
|
- (仅在生产模式下工作) 准备 `update.json` 或 `update-beta.json`
|
||||||
|
|
||||||
|
> [!note]
|
||||||
|
>
|
||||||
|
> **Dev & prod 两者有什么区别?**
|
||||||
|
>
|
||||||
|
> - 此环境变量存储在 `Zotero.${addonInstance}.data.env` 中,控制台输出在生产模式下被禁用.
|
||||||
|
> - 你可以根据此变量决定用户无法查看/使用的内容.
|
||||||
|
> - 在生产模式下,构建脚本将自动打包插件并更新 `update.json`.
|
||||||
|
|
||||||
|
### 5 发布(Release)
|
||||||
|
|
||||||
|
如果要构建和发布插件,运行如下指令:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
# A release-it command: version increase, npm run build, git push, and GitHub release
|
||||||
|
# release-it: https://github.com/release-it/release-it
|
||||||
|
npm run release
|
||||||
|
```
|
||||||
|
|
||||||
|
> [!note]
|
||||||
|
> 在此模板中,release-it 被配置为在本地升级版本、构建、推送提交和 git 标签,随后GitHub Action 将重新构建插件并将 XPI 发布到 GitHub Release.
|
||||||
|
>
|
||||||
|
> 如果你需要发布一个本地构建的 XPI,将 `package.json` 中的 `release-it.github.release` 设置为 `true`,然后移除 `.github/workflows/release.yml`. 此外,你还需要设置环境变量 `GITHUB_TOKEN`,获取 GitHub Token(https://github.com/settings/tokens).
|
||||||
|
|
||||||
|
#### 关于预发布
|
||||||
|
|
||||||
|
该模板将 `prerelease` 定义为插件的测试版,当你在 release-it 中选择 `prerelease` 版本 (版本号中带有 `-` ),构建脚本将创建一个 `update-beta.json` 给预发布版本使用,这将确保常规版本的用户不会自动更新到测试版,只有手动下载并安装了测试版的用户才能自动更新到下一个测试版. 当下一个正式版本更新时,脚本将同步更新 `update.json` 和 `update-beta.json`,这将使正式版和测试版用户都可以更新到最新的正式版.
|
||||||
|
|
||||||
|
> [!warning]
|
||||||
|
> 严格来说,区分 Zotero 6 和 Zotero 7 兼容的插件版本应该通过 `update.json` 的 `addons.__addonID__.updates[]` 中分别配置 `applications.zotero.strict_min_version`,这样 Zotero 才能正确识别,详情在 Zotero 7 开发文档(https://www.zotero.org/support/dev/zotero_7_for_developers#updaterdf_updatesjson)获取.
|
||||||
|
|
||||||
|
## Details 更多细节
|
||||||
|
|
||||||
|
### 关于Hooks(About Hooks)
|
||||||
|
|
||||||
|
> 可以在 [`src/hooks.ts`](https://github.com/windingwind/zotero-plugin-template/blob/main/src/hooks.ts) 中查看更多
|
||||||
|
|
||||||
|
1. 当在 Zotero 中触发安装/启用/启动时,`bootstrap.js` > `startup` 被调用
|
||||||
|
- 等待 Zotero 就绪
|
||||||
|
- 加载 `index.js` (插件代码的主入口,从 `index.ts` 中构建)
|
||||||
|
- 如果是 Zotero 7 以上的版本则注册资源
|
||||||
|
2. 主入口 `index.js` 中,插件对象被注入到 `Zotero` ,并且 `hooks.ts` > `onStartup` 被调用.
|
||||||
|
- 初始化插件需要的资源,包括通知监听器、首选项面板和UI元素.
|
||||||
|
3. 当在 Zotero 中触发卸载/禁用时,`bootstrap.js` > `shutdown` 被调用.
|
||||||
|
- `events.ts` > `onShutdown` 被调用. 移除 UI 元素、首选项面板或插件创建的任何内容.
|
||||||
|
- 移除脚本并释放资源.
|
||||||
|
|
||||||
|
### 关于全局变量(About Global Variables)
|
||||||
|
|
||||||
|
> 可以在 [`src/index.ts`](https://github.com/windingwind/zotero-plugin-template/blob/main/src/index.ts)中查看更多
|
||||||
|
|
||||||
|
bootstrap插件在沙盒中运行,但沙盒中没有默认的全局变量,例如 `Zotero` 或 `window` 等我们曾在overlay插件环境中使用的变量.
|
||||||
|
|
||||||
|
此模板将以下变量注册到全局范围:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
Zotero, ZoteroPane, Zotero_Tabs, window, document, rootURI, ztoolkit, addon;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 创建元素 API(Create Elements API)
|
||||||
|
|
||||||
|
插件模板为 bootstrap 插件提供了一些新的API. 我们有两个原因使用这些 API,而不是使用 `createElement/createElementNS`:
|
||||||
|
|
||||||
|
- 在 bootstrap 模式下,插件必须在推出(禁用或卸载)时清理所有 UI 元素,这非常麻烦. 使用 `createElement`,插件模板将维护这些元素. 仅仅在退出时 `unregisterAll` .
|
||||||
|
- Zotero 7 需要 createElement()/createElementNS() → createXULElement() 来表示其他的 XUL 元素,而 Zotero 6 并不支持 `createXULElement`. 类似于 React.createElement 的API `createElement` 检测 namespace(xul/html/svg) 并且自动创建元素,返回元素为对应的 TypeScript 元素类型.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
createElement(document, "div"); // returns HTMLDivElement
|
||||||
|
createElement(document, "hbox"); // returns XUL.Box
|
||||||
|
createElement(document, "button", { namespace: "xul" }); // manually set namespace. returns XUL.Button
|
||||||
|
```
|
||||||
|
|
||||||
|
### 关于 Zotero API(About Zotero API)
|
||||||
|
|
||||||
|
Zotero 文档已过时且不完整,克隆 https://github.com/zotero/zotero 并全局搜索关键字.
|
||||||
|
|
||||||
|
> ⭐[zotero-types](https://github.com/windingwind/zotero-types) 提供了最常用的 Zotero API,在默认情况下它被包含在此模板中. 你的 IDE 将为大多数的 API 提供提醒.
|
||||||
|
|
||||||
|
猜你需要:查找所需 API的技巧
|
||||||
|
|
||||||
|
在 `.xhtml`/`.flt` 文件中搜索 UI 标签,然后在 locale 文件中找到对应的键. ,然后在 `.js`/`.jsx` 文件中搜索此键.
|
||||||
|
|
||||||
|
### 目录结构(Directory Structure)
|
||||||
|
|
||||||
|
本部分展示了模板的目录结构.
|
||||||
|
|
||||||
|
- 所有的 `.js/.ts` 代码都在 `./src`;
|
||||||
|
- 插件配置文件:`./addon/manifest.json`;
|
||||||
|
- UI 文件: `./addon/chrome/content/*.xhtml`.
|
||||||
|
- 区域设置文件: `./addon/locale/**/*.flt`;
|
||||||
|
- 首选项文件: `./addon/prefs.js`;
|
||||||
|
> 不要在 `prefs.js` 中换行
|
||||||
|
|
||||||
|
```shell
|
||||||
|
.
|
||||||
|
|-- .eslintrc.json # eslint conf
|
||||||
|
|-- .gitattributes # git conf
|
||||||
|
|-- .github/ # github conf
|
||||||
|
|-- .gitignore # git conf
|
||||||
|
|-- .prettierrc # prettier conf
|
||||||
|
|-- .release-it.json # release-it conf
|
||||||
|
|-- .vscode # vs code conf
|
||||||
|
| |-- extensions.json
|
||||||
|
| |-- launch.json
|
||||||
|
| |-- setting.json
|
||||||
|
| `-- toolkit.code-snippets
|
||||||
|
|-- package-lock.json # npm conf
|
||||||
|
|-- package.json # npm conf
|
||||||
|
|-- LICENSE
|
||||||
|
|-- README.md
|
||||||
|
|-- addon
|
||||||
|
| |-- bootstrap.js # addon load/unload script, like a main.c
|
||||||
|
| |-- chrome
|
||||||
|
| | `-- content
|
||||||
|
| | |-- icons/
|
||||||
|
| | |-- preferences.xhtml # preference panel
|
||||||
|
| | `-- zoteroPane.css
|
||||||
|
| |-- locale # locale
|
||||||
|
| | |-- en-US
|
||||||
|
| | | |-- addon.ftl
|
||||||
|
| | | `-- preferences.ftl
|
||||||
|
| | `-- zh-CN
|
||||||
|
| | |-- addon.ftl
|
||||||
|
| | `-- preferences.ftl
|
||||||
|
| |-- manifest.json # addon config
|
||||||
|
| `-- prefs.js
|
||||||
|
|-- build/ # build dir
|
||||||
|
|-- scripts # scripts for dev
|
||||||
|
| |-- build.mjs # script to build plugin
|
||||||
|
| |-- scripts.mjs # scripts send to Zotero, such as reload, openDevTool, etc
|
||||||
|
| |-- server.mjs # script to start a development server
|
||||||
|
| |-- start.mjs # script to start Zotero process
|
||||||
|
| |-- stop.mjs # script to kill Zotero process
|
||||||
|
| |-- utils.mjs # utils functions for dev scripts
|
||||||
|
| |-- update-template.json # template of `update.json`
|
||||||
|
| `-- zotero-cmd-template.json # template of local env
|
||||||
|
|-- src # source code
|
||||||
|
| |-- addon.ts # base class
|
||||||
|
| |-- hooks.ts # lifecycle hooks
|
||||||
|
| |-- index.ts # main entry
|
||||||
|
| |-- modules # sub modules
|
||||||
|
| | |-- examples.ts
|
||||||
|
| | `-- preferenceScript.ts
|
||||||
|
| `-- utils # utilities
|
||||||
|
| |-- locale.ts
|
||||||
|
| |-- prefs.ts
|
||||||
|
| |-- wait.ts
|
||||||
|
| `-- window.ts
|
||||||
|
|-- tsconfig.json # https://code.visualstudio.com/docs/languages/jsconfig
|
||||||
|
|-- typings # ts typings
|
||||||
|
| `-- global.d.ts
|
||||||
|
`-- update.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Disclaimer 免责声明
|
||||||
|
|
||||||
|
在 AGPL 下使用此代码. 不提供任何保证. 遵守你所在地区的法律!
|
||||||
|
|
||||||
|
如果你想更改许可,请通过 <wyzlshx@foxmail.com> 与我联系.
|
||||||
131
package.json
131
package.json
|
|
@ -8,26 +8,17 @@
|
||||||
"addonRef": "zoterotldr",
|
"addonRef": "zoterotldr",
|
||||||
"addonInstance": "ZoteroTLDR",
|
"addonInstance": "ZoteroTLDR",
|
||||||
"prefsPrefix": "extensions.zotero.zoterotldr",
|
"prefsPrefix": "extensions.zotero.zoterotldr",
|
||||||
"releasepage": "https://github.com/syt2/zotero-tldr/releases/latest/download/zotero-tldr.xpi",
|
"releasepage": "https://github.com/syt2/zotero-tldr/releases",
|
||||||
"updaterdf": "https://raw.githubusercontent.com/syt2/zotero-tldr/main/update.json"
|
"updateJSON": "https://raw.githubusercontent.com/syt2/zotero-tldr/main/update.json"
|
||||||
},
|
},
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build-dev": "cross-env NODE_ENV=development node scripts/build.mjs",
|
"start": "node scripts/server.mjs",
|
||||||
"build-prod": "cross-env NODE_ENV=production node scripts/build.mjs",
|
"build": "tsc --noEmit && node scripts/build.mjs production",
|
||||||
"build": "concurrently -c auto npm:build-prod npm:tsc",
|
|
||||||
"tsc": "tsc --noEmit",
|
|
||||||
"start": "node scripts/start.mjs",
|
|
||||||
"start-watch": "npm run build-dev && concurrently -c auto npm:start npm:watch",
|
|
||||||
"stop": "node scripts/stop.mjs",
|
"stop": "node scripts/stop.mjs",
|
||||||
"restart-dev": "npm run build-dev && npm run stop && npm run start",
|
|
||||||
"restart-prod": "npm run build-prod && npm run stop && npm run start",
|
|
||||||
"restart": "npm run restart-dev",
|
|
||||||
"reload": "npm run build-dev && node scripts/reload.mjs",
|
|
||||||
"watch": "chokidar \"src/**\" \"addon/**\" -c \"npm run reload\"",
|
|
||||||
"release": "release-it",
|
|
||||||
"lint": "prettier --write . && eslint . --ext .ts --fix",
|
"lint": "prettier --write . && eslint . --ext .ts --fix",
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"release": "release-it --only-version --preReleaseId=beta",
|
||||||
"update-deps": "npm update --save"
|
"update-deps": "npm update --save"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|
@ -41,23 +32,103 @@
|
||||||
},
|
},
|
||||||
"homepage": "https://github.com/syt2/zotero-tldr#readme",
|
"homepage": "https://github.com/syt2/zotero-tldr#readme",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"zotero-plugin-toolkit": "^2.3.29"
|
"zotero-plugin-toolkit": "^2.3.15"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.4.2",
|
"@types/node": "^20.10.4",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
"@typescript-eslint/eslint-plugin": "^7.3.1",
|
||||||
"@typescript-eslint/parser": "^6.0.0",
|
"@typescript-eslint/parser": "^7.1.1",
|
||||||
"chokidar-cli": "^3.0.0",
|
"chokidar": "^3.5.3",
|
||||||
"compressing": "^1.9.0",
|
"compressing": "^1.10.0",
|
||||||
"concurrently": "^8.2.0",
|
"esbuild": "^0.20.1",
|
||||||
"cross-env": "^7.0.3",
|
"eslint": "^8.55.0",
|
||||||
"esbuild": "^0.18.12",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint": "^8.44.0",
|
"prettier": "^3.1.1",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"release-it": "^17.0.1",
|
||||||
"prettier": "^3.0.0",
|
"replace-in-file": "^7.0.2",
|
||||||
"release-it": "^16.1.0",
|
"typescript": "^5.3.3",
|
||||||
"replace-in-file": "^7.0.1",
|
"zotero-types": "^1.3.10"
|
||||||
"typescript": "^5.1.6",
|
},
|
||||||
"zotero-types": "^1.3.11"
|
"eslintConfig": {
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2021": true
|
||||||
|
},
|
||||||
|
"root": true,
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"overrides": [],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"@typescript-eslint/ban-ts-comment": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"ts-expect-error": "allow-with-description",
|
||||||
|
"ts-ignore": "allow-with-description",
|
||||||
|
"ts-nocheck": "allow-with-description",
|
||||||
|
"ts-check": "allow-with-description"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": [
|
||||||
|
"off",
|
||||||
|
{
|
||||||
|
"ignoreRestArgs": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/no-non-null-assertion": "off"
|
||||||
|
},
|
||||||
|
"ignorePatterns": [
|
||||||
|
"**/build/**",
|
||||||
|
"**/logs/**",
|
||||||
|
"**/dist/**",
|
||||||
|
"**/node_modules/**",
|
||||||
|
"**/scripts/**",
|
||||||
|
"**/*.js",
|
||||||
|
"**/*.bak"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"prettier": {
|
||||||
|
"printWidth": 80,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"endOfLine": "lf",
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.xhtml"
|
||||||
|
],
|
||||||
|
"options": {
|
||||||
|
"htmlWhitespaceSensitivity": "css"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"release-it": {
|
||||||
|
"git": {
|
||||||
|
"tagName": "v${version}"
|
||||||
|
},
|
||||||
|
"npm": {
|
||||||
|
"publish": false
|
||||||
|
},
|
||||||
|
"github": {
|
||||||
|
"release": false,
|
||||||
|
"assets": [
|
||||||
|
"build/*.xpi"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hooks": {
|
||||||
|
"before:init": "npm run lint",
|
||||||
|
"after:bump": "npm run build"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,121 +1,29 @@
|
||||||
import { build } from "esbuild";
|
import details from "../package.json" assert { type: "json" };
|
||||||
import { zip } from "compressing";
|
|
||||||
import path from "path";
|
|
||||||
import {
|
import {
|
||||||
existsSync,
|
Logger,
|
||||||
lstatSync,
|
clearFolder,
|
||||||
writeFileSync,
|
copyFileSync,
|
||||||
readFileSync,
|
copyFolderRecursiveSync,
|
||||||
mkdirSync,
|
dateFormat,
|
||||||
readdirSync,
|
} from "./utils.mjs";
|
||||||
rmSync,
|
import { zip } from "compressing";
|
||||||
renameSync,
|
import { build } from "esbuild";
|
||||||
} from "fs";
|
import { existsSync, readdirSync, renameSync } from "fs";
|
||||||
|
import path from "path";
|
||||||
import { env, exit } from "process";
|
import { env, exit } from "process";
|
||||||
import replaceInFile from "replace-in-file";
|
import replaceInFile from "replace-in-file";
|
||||||
|
|
||||||
const { replaceInFileSync } = replaceInFile;
|
const { replaceInFileSync } = replaceInFile;
|
||||||
import details from "../package.json" assert { type: "json" };
|
|
||||||
|
|
||||||
const { name, author, description, homepage, version, config } = details;
|
process.env.NODE_ENV =
|
||||||
|
process.argv[2] === "production" ? "production" : "development";
|
||||||
|
|
||||||
const t = new Date();
|
|
||||||
const buildTime = dateFormat("YYYY-mm-dd HH:MM:SS", new Date());
|
|
||||||
const buildDir = "build";
|
const buildDir = "build";
|
||||||
|
|
||||||
|
const { name, author, description, homepage, version, config } = details;
|
||||||
const isPreRelease = version.includes("-");
|
const isPreRelease = version.includes("-");
|
||||||
|
|
||||||
function copyFileSync(source, target) {
|
function replaceString(buildTime) {
|
||||||
var targetFile = target;
|
|
||||||
|
|
||||||
// If target is a directory, a new file with the same name will be created
|
|
||||||
if (existsSync(target)) {
|
|
||||||
if (lstatSync(target).isDirectory()) {
|
|
||||||
targetFile = path.join(target, path.basename(source));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
writeFileSync(targetFile, readFileSync(source));
|
|
||||||
}
|
|
||||||
|
|
||||||
function copyFolderRecursiveSync(source, target) {
|
|
||||||
var files = [];
|
|
||||||
|
|
||||||
// Check if folder needs to be created or integrated
|
|
||||||
var targetFolder = path.join(target, path.basename(source));
|
|
||||||
if (!existsSync(targetFolder)) {
|
|
||||||
mkdirSync(targetFolder);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy
|
|
||||||
if (lstatSync(source).isDirectory()) {
|
|
||||||
files = readdirSync(source);
|
|
||||||
files.forEach(function (file) {
|
|
||||||
var curSource = path.join(source, file);
|
|
||||||
if (lstatSync(curSource).isDirectory()) {
|
|
||||||
copyFolderRecursiveSync(curSource, targetFolder);
|
|
||||||
} else {
|
|
||||||
copyFileSync(curSource, targetFolder);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearFolder(target) {
|
|
||||||
if (existsSync(target)) {
|
|
||||||
rmSync(target, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
mkdirSync(target, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
function dateFormat(fmt, date) {
|
|
||||||
let ret;
|
|
||||||
const opt = {
|
|
||||||
"Y+": date.getFullYear().toString(),
|
|
||||||
"m+": (date.getMonth() + 1).toString(),
|
|
||||||
"d+": date.getDate().toString(),
|
|
||||||
"H+": date.getHours().toString(),
|
|
||||||
"M+": date.getMinutes().toString(),
|
|
||||||
"S+": date.getSeconds().toString(),
|
|
||||||
};
|
|
||||||
for (let k in opt) {
|
|
||||||
ret = new RegExp("(" + k + ")").exec(fmt);
|
|
||||||
if (ret) {
|
|
||||||
fmt = fmt.replace(
|
|
||||||
ret[1],
|
|
||||||
ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, "0"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt;
|
|
||||||
}
|
|
||||||
|
|
||||||
function renameLocaleFiles() {
|
|
||||||
const localeDir = path.join(buildDir, "addon/locale");
|
|
||||||
const localeFolders = readdirSync(localeDir, { withFileTypes: true })
|
|
||||||
.filter((dirent) => dirent.isDirectory())
|
|
||||||
.map((dirent) => dirent.name);
|
|
||||||
|
|
||||||
for (const localeSubFolder of localeFolders) {
|
|
||||||
const localeSubDir = path.join(localeDir, localeSubFolder);
|
|
||||||
const localeSubFiles = readdirSync(localeSubDir, {
|
|
||||||
withFileTypes: true,
|
|
||||||
})
|
|
||||||
.filter((dirent) => dirent.isFile())
|
|
||||||
.map((dirent) => dirent.name);
|
|
||||||
|
|
||||||
for (const localeSubFile of localeSubFiles) {
|
|
||||||
if (localeSubFile.endsWith(".ftl")) {
|
|
||||||
renameSync(
|
|
||||||
path.join(localeSubDir, localeSubFile),
|
|
||||||
path.join(localeSubDir, `${config.addonRef}-${localeSubFile}`),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function replaceString() {
|
|
||||||
const replaceFrom = [
|
const replaceFrom = [
|
||||||
/__author__/g,
|
/__author__/g,
|
||||||
/__description__/g,
|
/__description__/g,
|
||||||
|
|
@ -125,15 +33,20 @@ function replaceString() {
|
||||||
];
|
];
|
||||||
const replaceTo = [author, description, homepage, version, buildTime];
|
const replaceTo = [author, description, homepage, version, buildTime];
|
||||||
|
|
||||||
|
config.updateURL = isPreRelease
|
||||||
|
? config.updateJSON.replace("update.json", "update-beta.json")
|
||||||
|
: config.updateJSON;
|
||||||
|
|
||||||
replaceFrom.push(
|
replaceFrom.push(
|
||||||
...Object.keys(config).map((k) => new RegExp(`__${k}__`, "g")),
|
...Object.keys(config).map((k) => new RegExp(`__${k}__`, "g")),
|
||||||
);
|
);
|
||||||
replaceTo.push(...Object.values(config));
|
replaceTo.push(...Object.values(config));
|
||||||
|
|
||||||
const optionsAddon = {
|
const replaceResult = replaceInFileSync({
|
||||||
files: [
|
files: [
|
||||||
`${buildDir}/addon/**/*.xhtml`,
|
`${buildDir}/addon/**/*.xhtml`,
|
||||||
`${buildDir}/addon/**/*.html`,
|
`${buildDir}/addon/**/*.html`,
|
||||||
|
`${buildDir}/addon/**/*.css`,
|
||||||
`${buildDir}/addon/**/*.json`,
|
`${buildDir}/addon/**/*.json`,
|
||||||
`${buildDir}/addon/prefs.js`,
|
`${buildDir}/addon/prefs.js`,
|
||||||
`${buildDir}/addon/manifest.json`,
|
`${buildDir}/addon/manifest.json`,
|
||||||
|
|
@ -142,137 +55,189 @@ function replaceString() {
|
||||||
from: replaceFrom,
|
from: replaceFrom,
|
||||||
to: replaceTo,
|
to: replaceTo,
|
||||||
countMatches: true,
|
countMatches: true,
|
||||||
};
|
|
||||||
|
|
||||||
if (!isPreRelease) {
|
|
||||||
optionsAddon.files.push("update.json");
|
|
||||||
}
|
|
||||||
|
|
||||||
const replaceResult = replaceInFileSync(optionsAddon);
|
|
||||||
|
|
||||||
const localeMessage = new Set();
|
|
||||||
const localeMessageMiss = new Set();
|
|
||||||
|
|
||||||
const replaceResultFlt = replaceInFileSync({
|
|
||||||
files: [`${buildDir}/addon/locale/**/*.ftl`],
|
|
||||||
processor: (fltContent) => {
|
|
||||||
const lines = fltContent.split("\n");
|
|
||||||
const prefixedLines = lines.map((line) => {
|
|
||||||
// https://regex101.com/r/lQ9x5p/1
|
|
||||||
const match = line.match(
|
|
||||||
/^(?<message>[a-zA-Z]\S*)([ ]*=[ ]*)(?<pattern>.*)$/m,
|
|
||||||
);
|
|
||||||
if (match) {
|
|
||||||
localeMessage.add(match.groups.message);
|
|
||||||
return `${config.addonRef}-${line}`;
|
|
||||||
} else {
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return prefixedLines.join("\n");
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const replaceResultXhtml = replaceInFileSync({
|
// Logger.debug(
|
||||||
files: [`${buildDir}/addon/**/*.xhtml`],
|
// "[Build] Run replace in ",
|
||||||
|
// replaceResult.filter((f) => f.hasChanged).map((f) => `${f.file} : ${f.numReplacements} / ${f.numMatches}`),
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareLocaleFiles() {
|
||||||
|
// Prefix Fluent messages in xhtml
|
||||||
|
const MessagesInHTML = new Set();
|
||||||
|
replaceInFileSync({
|
||||||
|
files: [`${buildDir}/addon/**/*.xhtml`, `${buildDir}/addon/**/*.html`],
|
||||||
processor: (input) => {
|
processor: (input) => {
|
||||||
const matchs = [...input.matchAll(/(data-l10n-id)="(\S*)"/g)];
|
const matchs = [...input.matchAll(/(data-l10n-id)="(\S*)"/g)];
|
||||||
matchs.map((match) => {
|
matchs.map((match) => {
|
||||||
if (localeMessage.has(match[2])) {
|
input = input.replace(
|
||||||
input = input.replace(
|
match[0],
|
||||||
match[0],
|
`${match[1]}="${config.addonRef}-${match[2]}"`,
|
||||||
`${match[1]}="${config.addonRef}-${match[2]}"`,
|
);
|
||||||
);
|
MessagesInHTML.add(match[2]);
|
||||||
} else {
|
|
||||||
localeMessageMiss.add(match[2]);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return input;
|
return input;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(
|
// Walk the sub folders of `build/addon/locale`
|
||||||
"[Build] Run replace in ",
|
const localesPath = path.join(buildDir, "addon/locale"),
|
||||||
replaceResult
|
localeNames = readdirSync(localesPath, { withFileTypes: true })
|
||||||
.filter((f) => f.hasChanged)
|
.filter((dirent) => dirent.isDirectory())
|
||||||
.map((f) => `${f.file} : ${f.numReplacements} / ${f.numMatches}`),
|
.map((dirent) => dirent.name);
|
||||||
replaceResultFlt.filter((f) => f.hasChanged).map((f) => `${f.file} : OK`),
|
|
||||||
replaceResultXhtml.filter((f) => f.hasChanged).map((f) => `${f.file} : OK`),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (localeMessageMiss.size !== 0) {
|
for (const localeName of localeNames) {
|
||||||
console.warn(
|
const localePath = path.join(localesPath, localeName);
|
||||||
`[Build] [Warn] Fluent message [${new Array(
|
const ftlFiles = readdirSync(localePath, {
|
||||||
...localeMessageMiss,
|
withFileTypes: true,
|
||||||
)}] do not exsit in addon's locale files.`,
|
})
|
||||||
);
|
.filter((dirent) => dirent.isFile())
|
||||||
|
.map((dirent) => dirent.name);
|
||||||
|
|
||||||
|
// rename *.ftl to addonRef-*.ftl
|
||||||
|
for (const ftlFile of ftlFiles) {
|
||||||
|
if (ftlFile.endsWith(".ftl")) {
|
||||||
|
renameSync(
|
||||||
|
path.join(localePath, ftlFile),
|
||||||
|
path.join(localePath, `${config.addonRef}-${ftlFile}`),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefix Fluent messages in each ftl
|
||||||
|
const MessageInThisLang = new Set();
|
||||||
|
replaceInFileSync({
|
||||||
|
files: [`${buildDir}/addon/locale/${localeName}/*.ftl`],
|
||||||
|
processor: (fltContent) => {
|
||||||
|
const lines = fltContent.split("\n");
|
||||||
|
const prefixedLines = lines.map((line) => {
|
||||||
|
// https://regex101.com/r/lQ9x5p/1
|
||||||
|
const match = line.match(
|
||||||
|
/^(?<message>[a-zA-Z]\S*)([ ]*=[ ]*)(?<pattern>.*)$/m,
|
||||||
|
);
|
||||||
|
if (match) {
|
||||||
|
MessageInThisLang.add(match.groups.message);
|
||||||
|
return `${config.addonRef}-${line}`;
|
||||||
|
} else {
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return prefixedLines.join("\n");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// If a message in xhtml but not in ftl of current language, log it
|
||||||
|
MessagesInHTML.forEach((message) => {
|
||||||
|
if (!MessageInThisLang.has(message)) {
|
||||||
|
Logger.error(`[Build] ${message} don't exist in ${localeName}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function esbuild() {
|
function prepareUpdateJson() {
|
||||||
await build({
|
// If it is a pre-release, use update-beta.json
|
||||||
entryPoints: ["src/index.ts"],
|
if (!isPreRelease) {
|
||||||
define: {
|
copyFileSync("scripts/update-template.json", "update.json");
|
||||||
__env__: `"${env.NODE_ENV}"`,
|
}
|
||||||
},
|
if (existsSync("update-beta.json") || isPreRelease) {
|
||||||
bundle: true,
|
copyFileSync("scripts/update-template.json", "update-beta.json");
|
||||||
target: "firefox102",
|
}
|
||||||
outfile: path.join(
|
|
||||||
buildDir,
|
const updateLink =
|
||||||
`addon/chrome/content/scripts/${config.addonRef}.js`,
|
config.updateLink ?? isPreRelease
|
||||||
),
|
? `${config.releasePage}/download/v${version}/${name}.xpi`
|
||||||
// Don't turn minify on
|
: `${config.releasePage}/latest/download/${name}.xpi`;
|
||||||
// minify: true,
|
|
||||||
}).catch(() => exit(1));
|
const replaceResult = replaceInFileSync({
|
||||||
|
files: [
|
||||||
|
"update-beta.json",
|
||||||
|
isPreRelease ? "pass" : "update.json",
|
||||||
|
`${buildDir}/addon/manifest.json`,
|
||||||
|
],
|
||||||
|
from: [
|
||||||
|
/__addonID__/g,
|
||||||
|
/__buildVersion__/g,
|
||||||
|
/__updateLink__/g,
|
||||||
|
/__updateURL__/g,
|
||||||
|
],
|
||||||
|
to: [config.addonID, version, updateLink, config.updateURL],
|
||||||
|
countMatches: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
Logger.debug(
|
||||||
|
`[Build] Prepare Update.json for ${
|
||||||
|
isPreRelease
|
||||||
|
? "\u001b[31m Prerelease \u001b[0m"
|
||||||
|
: "\u001b[32m Release \u001b[0m"
|
||||||
|
}`,
|
||||||
|
replaceResult
|
||||||
|
.filter((f) => f.hasChanged)
|
||||||
|
.map((f) => `${f.file} : ${f.numReplacements} / ${f.numMatches}`),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
export const esbuildOptions = {
|
||||||
console.log(
|
entryPoints: ["src/index.ts"],
|
||||||
|
define: {
|
||||||
|
__env__: `"${env.NODE_ENV}"`,
|
||||||
|
},
|
||||||
|
bundle: true,
|
||||||
|
target: "firefox102",
|
||||||
|
outfile: path.join(
|
||||||
|
buildDir,
|
||||||
|
`addon/chrome/content/scripts/${config.addonRef}.js`,
|
||||||
|
),
|
||||||
|
// Don't turn minify on
|
||||||
|
minify: env.NODE_ENV === "production",
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function main() {
|
||||||
|
const t = new Date();
|
||||||
|
const buildTime = dateFormat("YYYY-mm-dd HH:MM:SS", new Date());
|
||||||
|
|
||||||
|
Logger.info(
|
||||||
`[Build] BUILD_DIR=${buildDir}, VERSION=${version}, BUILD_TIME=${buildTime}, ENV=${[
|
`[Build] BUILD_DIR=${buildDir}, VERSION=${version}, BUILD_TIME=${buildTime}, ENV=${[
|
||||||
env.NODE_ENV,
|
env.NODE_ENV,
|
||||||
]}`,
|
]}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
clearFolder(buildDir);
|
clearFolder(buildDir);
|
||||||
|
|
||||||
copyFolderRecursiveSync("addon", buildDir);
|
copyFolderRecursiveSync("addon", buildDir);
|
||||||
|
|
||||||
if (isPreRelease) {
|
Logger.debug("[Build] Replacing");
|
||||||
console.log(
|
replaceString(buildTime);
|
||||||
"[Build] [Warn] Running in pre-release mode. update.json will not be replaced.",
|
|
||||||
|
Logger.debug("[Build] Preparing locale files");
|
||||||
|
prepareLocaleFiles();
|
||||||
|
|
||||||
|
Logger.debug("[Build] Running esbuild");
|
||||||
|
await build(esbuildOptions);
|
||||||
|
|
||||||
|
Logger.debug("[Build] Addon prepare OK");
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV === "production") {
|
||||||
|
Logger.debug("[Build] Packing Addon");
|
||||||
|
await zip.compressDir(
|
||||||
|
path.join(buildDir, "addon"),
|
||||||
|
path.join(buildDir, `${name}.xpi`),
|
||||||
|
{
|
||||||
|
ignoreBase: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
prepareUpdateJson();
|
||||||
|
|
||||||
|
Logger.debug(
|
||||||
|
`[Build] Finished in ${(new Date().getTime() - t.getTime()) / 1000} s.`,
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
copyFileSync("update-template.json", "update.json");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await esbuild();
|
|
||||||
|
|
||||||
console.log("[Build] Run esbuild OK");
|
|
||||||
|
|
||||||
replaceString();
|
|
||||||
|
|
||||||
console.log("[Build] Replace OK");
|
|
||||||
|
|
||||||
// Walk the builds/addon/locale folder's sub folders and rename *.ftl to addonRef-*.ftl
|
|
||||||
renameLocaleFiles();
|
|
||||||
|
|
||||||
console.log("[Build] Addon prepare OK");
|
|
||||||
|
|
||||||
await zip.compressDir(
|
|
||||||
path.join(buildDir, "addon"),
|
|
||||||
path.join(buildDir, `${name}.xpi`),
|
|
||||||
{
|
|
||||||
ignoreBase: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("[Build] Addon pack OK");
|
|
||||||
console.log(
|
|
||||||
`[Build] Finished in ${(new Date().getTime() - t.getTime()) / 1000} s.`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((err) => {
|
if (process.env.NODE_ENV === "production") {
|
||||||
console.log(err);
|
main().catch((err) => {
|
||||||
exit(1);
|
Logger.error(err);
|
||||||
});
|
exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
import { exit } from "process";
|
|
||||||
import { execSync } from "child_process";
|
|
||||||
import details from "../package.json" assert { type: "json" };
|
|
||||||
import cmd from "./zotero-cmd.json" assert { type: "json" };
|
|
||||||
|
|
||||||
const { addonID, addonName } = details.config;
|
|
||||||
const { version } = details;
|
|
||||||
const { zoteroBinPath, profilePath } = cmd.exec;
|
|
||||||
|
|
||||||
const startZotero = `"${zoteroBinPath}" --debugger --purgecaches -profile "${profilePath}"`;
|
|
||||||
|
|
||||||
const script = `
|
|
||||||
(async () => {
|
|
||||||
Services.obs.notifyObservers(null, "startupcache-invalidate", null);
|
|
||||||
const { AddonManager } = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
|
|
||||||
const addon = await AddonManager.getAddonByID("${addonID}");
|
|
||||||
await addon.reload();
|
|
||||||
const progressWindow = new Zotero.ProgressWindow({ closeOnClick: true });
|
|
||||||
progressWindow.changeHeadline("${addonName} Hot Reload");
|
|
||||||
progressWindow.progress = new progressWindow.ItemProgress(
|
|
||||||
"chrome://zotero/skin/tick.png",
|
|
||||||
"VERSION=${version}, BUILD=${new Date().toLocaleString()}. By zotero-plugin-toolkit"
|
|
||||||
);
|
|
||||||
progressWindow.progress.setProgress(100);
|
|
||||||
progressWindow.show();
|
|
||||||
progressWindow.startCloseTimer(5000);
|
|
||||||
})()`;
|
|
||||||
|
|
||||||
const url = `zotero://ztoolkit-debug/?run=${encodeURIComponent(script)}`;
|
|
||||||
|
|
||||||
const command = `${startZotero} -url "${url}"`;
|
|
||||||
|
|
||||||
execSync(command);
|
|
||||||
exit(0);
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import details from "../package.json" assert { type: "json" };
|
||||||
|
|
||||||
|
const { addonID, addonName } = details.config;
|
||||||
|
const { version } = details;
|
||||||
|
|
||||||
|
export const reloadScript = `
|
||||||
|
(async () => {
|
||||||
|
Services.obs.notifyObservers(null, "startupcache-invalidate", null);
|
||||||
|
const { AddonManager } = ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
|
||||||
|
const addon = await AddonManager.getAddonByID("${addonID}");
|
||||||
|
await addon.reload();
|
||||||
|
const progressWindow = new Zotero.ProgressWindow({ closeOnClick: true });
|
||||||
|
progressWindow.changeHeadline("${addonName} Hot Reload");
|
||||||
|
progressWindow.progress = new progressWindow.ItemProgress(
|
||||||
|
"chrome://zotero/skin/tick.png",
|
||||||
|
"VERSION=${version}, BUILD=${new Date().toLocaleString()}. By zotero-plugin-toolkit"
|
||||||
|
);
|
||||||
|
progressWindow.progress.setProgress(100);
|
||||||
|
progressWindow.show();
|
||||||
|
progressWindow.startCloseTimer(5000);
|
||||||
|
})()`;
|
||||||
|
|
||||||
|
export const openDevToolScript = `
|
||||||
|
(async () => {
|
||||||
|
|
||||||
|
// const { BrowserToolboxLauncher } = ChromeUtils.import(
|
||||||
|
// "resource://devtools/client/framework/browser-toolbox/Launcher.jsm",
|
||||||
|
// );
|
||||||
|
// BrowserToolboxLauncher.init();
|
||||||
|
// TODO: Use the above code to open the devtool after https://github.com/zotero/zotero/pull/3387
|
||||||
|
|
||||||
|
Zotero.Prefs.set("devtools.debugger.remote-enabled", true, true);
|
||||||
|
Zotero.Prefs.set("devtools.debugger.remote-port", 6100, true);
|
||||||
|
Zotero.Prefs.set("devtools.debugger.prompt-connection", false, true);
|
||||||
|
Zotero.Prefs.set("devtools.debugger.chrome-debugging-websocket", false, true);
|
||||||
|
|
||||||
|
env =
|
||||||
|
Services.env ||
|
||||||
|
Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
|
||||||
|
|
||||||
|
env.set("MOZ_BROWSER_TOOLBOX_PORT", 6100);
|
||||||
|
Zotero.openInViewer(
|
||||||
|
"chrome://devtools/content/framework/browser-toolbox/window.html",
|
||||||
|
{
|
||||||
|
onLoad: (doc) => {
|
||||||
|
doc.querySelector("#status-message-container").style.visibility =
|
||||||
|
"collapse";
|
||||||
|
let toolboxBody;
|
||||||
|
waitUntil(
|
||||||
|
() => {
|
||||||
|
toolboxBody = doc
|
||||||
|
.querySelector(".devtools-toolbox-browsertoolbox-iframe")
|
||||||
|
?.contentDocument?.querySelector(".theme-body");
|
||||||
|
return toolboxBody;
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
toolboxBody.style = "pointer-events: all !important";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function waitUntil(condition, callback, interval = 100, timeout = 10000) {
|
||||||
|
const start = Date.now();
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
if (condition()) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
callback();
|
||||||
|
} else if (Date.now() - start > timeout) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
}
|
||||||
|
}, interval);
|
||||||
|
}
|
||||||
|
})()`;
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
import { main as build, esbuildOptions } from "./build.mjs";
|
||||||
|
import { openDevToolScript, reloadScript } from "./scripts.mjs";
|
||||||
|
import { main as startZotero } from "./start.mjs";
|
||||||
|
import { Logger } from "./utils.mjs";
|
||||||
|
import cmd from "./zotero-cmd.json" assert { type: "json" };
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import chokidar from "chokidar";
|
||||||
|
import { context } from "esbuild";
|
||||||
|
import { exit } from "process";
|
||||||
|
|
||||||
|
process.env.NODE_ENV = "development";
|
||||||
|
|
||||||
|
const { zoteroBinPath, profilePath } = cmd.exec;
|
||||||
|
|
||||||
|
const startZoteroCmd = `"${zoteroBinPath}" --debugger --purgecaches -profile "${profilePath}"`;
|
||||||
|
|
||||||
|
async function watch() {
|
||||||
|
const watcher = chokidar.watch(["src/**", "addon/**"], {
|
||||||
|
ignored: /(^|[\/\\])\../, // ignore dotfiles
|
||||||
|
persistent: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
let esbuildCTX = await context(esbuildOptions);
|
||||||
|
|
||||||
|
watcher
|
||||||
|
.on("ready", () => {
|
||||||
|
Logger.info("Server Ready! \n");
|
||||||
|
})
|
||||||
|
.on("change", async (path) => {
|
||||||
|
Logger.info(`${path} changed.`);
|
||||||
|
if (path.startsWith("src")) {
|
||||||
|
await esbuildCTX.rebuild();
|
||||||
|
} else if (path.startsWith("addon")) {
|
||||||
|
await build()
|
||||||
|
// Do not abort the watcher when errors occur in builds triggered by the watcher.
|
||||||
|
.catch((err) => {
|
||||||
|
Logger.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// reload
|
||||||
|
reload();
|
||||||
|
})
|
||||||
|
.on("error", (err) => {
|
||||||
|
Logger.error("Server start failed!", err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function reload() {
|
||||||
|
Logger.debug("Reloading...");
|
||||||
|
const url = `zotero://ztoolkit-debug/?run=${encodeURIComponent(
|
||||||
|
reloadScript,
|
||||||
|
)}`;
|
||||||
|
const command = `${startZoteroCmd} -url "${url}"`;
|
||||||
|
execSync(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDevTool() {
|
||||||
|
Logger.debug("Open dev tools...");
|
||||||
|
const url = `zotero://ztoolkit-debug/?run=${encodeURIComponent(
|
||||||
|
openDevToolScript,
|
||||||
|
)}`;
|
||||||
|
const command = `${startZoteroCmd} -url "${url}"`;
|
||||||
|
execSync(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
// build
|
||||||
|
await build();
|
||||||
|
|
||||||
|
// start Zotero
|
||||||
|
startZotero(openDevTool);
|
||||||
|
|
||||||
|
// watch
|
||||||
|
await watch();
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
Logger.error(err);
|
||||||
|
// execSync("node scripts/stop.mjs");
|
||||||
|
exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGINT", (code) => {
|
||||||
|
execSync("node scripts/stop.mjs");
|
||||||
|
Logger.info(`Server terminated with signal ${code}.`);
|
||||||
|
exit(0);
|
||||||
|
});
|
||||||
|
|
@ -1,33 +1,39 @@
|
||||||
import { execSync } from "child_process";
|
|
||||||
import { exit } from "process";
|
|
||||||
import { existsSync, writeFileSync, readFileSync, mkdirSync } from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import details from "../package.json" assert { type: "json" };
|
import details from "../package.json" assert { type: "json" };
|
||||||
|
import { Logger } from "./utils.mjs";
|
||||||
import cmd from "./zotero-cmd.json" assert { type: "json" };
|
import cmd from "./zotero-cmd.json" assert { type: "json" };
|
||||||
|
import { spawn } from "child_process";
|
||||||
|
import { existsSync, readFileSync, writeFileSync, rmSync } from "fs";
|
||||||
|
import { clearFolder } from "./utils.mjs";
|
||||||
|
import path from "path";
|
||||||
|
import { exit } from "process";
|
||||||
|
|
||||||
const { addonID } = details.config;
|
const { addonID } = details.config;
|
||||||
const { zoteroBinPath, profilePath, dataDir } = cmd.exec;
|
const { zoteroBinPath, profilePath, dataDir } = cmd.exec;
|
||||||
|
|
||||||
|
// Keep in sync with the addon's onStartup
|
||||||
|
const loadDevToolWhen = `Plugin ${addonID} startup`;
|
||||||
|
|
||||||
|
const logPath = "logs";
|
||||||
|
const logFilePath = path.join(logPath, "zotero.log");
|
||||||
|
|
||||||
if (!existsSync(zoteroBinPath)) {
|
if (!existsSync(zoteroBinPath)) {
|
||||||
throw new Error("Zotero binary does not exist.");
|
throw new Error("Zotero binary does not exist.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existsSync(profilePath)) {
|
if (!existsSync(profilePath)) {
|
||||||
|
throw new Error("The given Zotero profile does not exist.");
|
||||||
|
}
|
||||||
|
|
||||||
|
function prepareDevEnv() {
|
||||||
const addonProxyFilePath = path.join(profilePath, `extensions/${addonID}`);
|
const addonProxyFilePath = path.join(profilePath, `extensions/${addonID}`);
|
||||||
const buildPath = path.resolve("build/addon");
|
const buildPath = path.resolve("build/addon");
|
||||||
|
|
||||||
if (!existsSync(path.join(buildPath, "./manifest.json"))) {
|
|
||||||
throw new Error(
|
|
||||||
`The built file does not exist, maybe you need to build the addon first.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function writeAddonProxyFile() {
|
function writeAddonProxyFile() {
|
||||||
writeFileSync(addonProxyFilePath, buildPath);
|
writeFileSync(addonProxyFilePath, buildPath);
|
||||||
console.log(
|
Logger.debug(
|
||||||
`[info] Addon proxy file has been updated.
|
`Addon proxy file has been updated.
|
||||||
File path: ${addonProxyFilePath}
|
File path: ${addonProxyFilePath}
|
||||||
Addon path: ${buildPath} `,
|
Addon path: ${buildPath} `,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,15 +42,14 @@ if (existsSync(profilePath)) {
|
||||||
writeAddonProxyFile();
|
writeAddonProxyFile();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (
|
|
||||||
existsSync(profilePath) &&
|
|
||||||
!existsSync(path.join(profilePath, "extensions"))
|
|
||||||
) {
|
|
||||||
mkdirSync(path.join(profilePath, "extensions"));
|
|
||||||
}
|
|
||||||
writeAddonProxyFile();
|
writeAddonProxyFile();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addonXpiFilePath = path.join(profilePath, `extensions/${addonID}.xpi`);
|
||||||
|
if (existsSync(addonXpiFilePath)) {
|
||||||
|
rmSync(addonXpiFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
const prefsPath = path.join(profilePath, "prefs.js");
|
const prefsPath = path.join(profilePath, "prefs.js");
|
||||||
if (existsSync(prefsPath)) {
|
if (existsSync(prefsPath)) {
|
||||||
const PrefsLines = readFileSync(prefsPath, "utf-8").split("\n");
|
const PrefsLines = readFileSync(prefsPath, "utf-8").split("\n");
|
||||||
|
|
@ -56,19 +61,59 @@ if (existsSync(profilePath)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (line.includes("extensions.zotero.dataDir") && dataDir !== "") {
|
if (line.includes("extensions.zotero.dataDir") && dataDir !== "") {
|
||||||
return `user_pref("extensions.zotero.dataDir", "${dataDir}");`;
|
return `user_pref("extensions.zotero.dataDir", "${dataDir.replace(/\\\\?/g, "\\\\")}");`;
|
||||||
}
|
}
|
||||||
return line;
|
return line;
|
||||||
});
|
});
|
||||||
const updatedPrefs = filteredLines.join("\n");
|
const updatedPrefs = filteredLines.join("\n");
|
||||||
writeFileSync(prefsPath, updatedPrefs, "utf-8");
|
writeFileSync(prefsPath, updatedPrefs, "utf-8");
|
||||||
console.log("[info] The <profile>/prefs.js has been modified.");
|
Logger.debug("The <profile>/prefs.js has been modified.");
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
throw new Error("The given Zotero profile does not exist.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const startZotero = `"${zoteroBinPath}" --debugger --purgecaches -profile "${profilePath}"`;
|
function prepareLog() {
|
||||||
|
clearFolder(logPath);
|
||||||
|
writeFileSync(logFilePath, "");
|
||||||
|
}
|
||||||
|
|
||||||
execSync(startZotero);
|
export function main(callback) {
|
||||||
exit(0);
|
let isZoteroReady = false;
|
||||||
|
|
||||||
|
prepareDevEnv();
|
||||||
|
|
||||||
|
prepareLog();
|
||||||
|
|
||||||
|
const zoteroProcess = spawn(zoteroBinPath, [
|
||||||
|
"--debugger",
|
||||||
|
"--purgecaches",
|
||||||
|
"-profile",
|
||||||
|
profilePath,
|
||||||
|
]);
|
||||||
|
|
||||||
|
zoteroProcess.stdout.on("data", (data) => {
|
||||||
|
if (!isZoteroReady && data.toString().includes(loadDevToolWhen)) {
|
||||||
|
isZoteroReady = true;
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
writeFileSync(logFilePath, data, {
|
||||||
|
flag: "a",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
zoteroProcess.stderr.on("data", (data) => {
|
||||||
|
writeFileSync(logFilePath, data, {
|
||||||
|
flag: "a",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
zoteroProcess.on("close", (code) => {
|
||||||
|
Logger.info(`Zotero terminated with code ${code}.`);
|
||||||
|
exit(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("SIGINT", () => {
|
||||||
|
// Handle interrupt signal (Ctrl+C) to gracefully terminate Zotero process
|
||||||
|
zoteroProcess.kill();
|
||||||
|
exit();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,26 @@
|
||||||
import process from "process";
|
import { Logger, isRunning } from "./utils.mjs";
|
||||||
import { execSync } from "child_process";
|
|
||||||
import cmd from "./zotero-cmd.json" assert { type: "json" };
|
import cmd from "./zotero-cmd.json" assert { type: "json" };
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import process from "process";
|
||||||
|
|
||||||
const { killZoteroWindows, killZoteroUnix } = cmd;
|
const { killZoteroWindows, killZoteroUnix } = cmd;
|
||||||
|
|
||||||
try {
|
isRunning("zotero", (status) => {
|
||||||
if (process.platform === "win32") {
|
if (status) {
|
||||||
execSync(killZoteroWindows);
|
killZotero();
|
||||||
} else {
|
} else {
|
||||||
execSync(killZoteroUnix);
|
Logger.warn("No Zotero running.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function killZotero() {
|
||||||
|
try {
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
execSync(killZoteroWindows);
|
||||||
|
} else {
|
||||||
|
execSync(killZoteroUnix);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error(e);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
"updates": [
|
"updates": [
|
||||||
{
|
{
|
||||||
"version": "__buildVersion__",
|
"version": "__buildVersion__",
|
||||||
"update_link": "__releasepage__",
|
"update_link": "__updateLink__",
|
||||||
"applications": {
|
"applications": {
|
||||||
"zotero": {
|
"zotero": {
|
||||||
"strict_min_version": "6.999"
|
"strict_min_version": "6.999"
|
||||||
|
|
@ -0,0 +1,129 @@
|
||||||
|
import { exec } from "child_process";
|
||||||
|
import {
|
||||||
|
existsSync,
|
||||||
|
lstatSync,
|
||||||
|
mkdirSync,
|
||||||
|
readFileSync,
|
||||||
|
readdirSync,
|
||||||
|
rmSync,
|
||||||
|
writeFileSync,
|
||||||
|
} from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export function copyFileSync(source, target) {
|
||||||
|
var targetFile = target;
|
||||||
|
|
||||||
|
// If target is a directory, a new file with the same name will be created
|
||||||
|
if (existsSync(target)) {
|
||||||
|
if (lstatSync(target).isDirectory()) {
|
||||||
|
targetFile = path.join(target, path.basename(source));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(targetFile, readFileSync(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function copyFolderRecursiveSync(source, target) {
|
||||||
|
var files = [];
|
||||||
|
|
||||||
|
// Check if folder needs to be created or integrated
|
||||||
|
var targetFolder = path.join(target, path.basename(source));
|
||||||
|
if (!existsSync(targetFolder)) {
|
||||||
|
mkdirSync(targetFolder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy
|
||||||
|
if (lstatSync(source).isDirectory()) {
|
||||||
|
files = readdirSync(source);
|
||||||
|
files.forEach(function (file) {
|
||||||
|
var curSource = path.join(source, file);
|
||||||
|
if (lstatSync(curSource).isDirectory()) {
|
||||||
|
copyFolderRecursiveSync(curSource, targetFolder);
|
||||||
|
} else {
|
||||||
|
copyFileSync(curSource, targetFolder);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearFolder(target) {
|
||||||
|
if (existsSync(target)) {
|
||||||
|
rmSync(target, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdirSync(target, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function dateFormat(fmt, date) {
|
||||||
|
let ret;
|
||||||
|
const opt = {
|
||||||
|
"Y+": date.getFullYear().toString(),
|
||||||
|
"m+": (date.getMonth() + 1).toString(),
|
||||||
|
"d+": date.getDate().toString(),
|
||||||
|
"H+": date.getHours().toString(),
|
||||||
|
"M+": date.getMinutes().toString(),
|
||||||
|
"S+": date.getSeconds().toString(),
|
||||||
|
};
|
||||||
|
for (let k in opt) {
|
||||||
|
ret = new RegExp("(" + k + ")").exec(fmt);
|
||||||
|
if (ret) {
|
||||||
|
fmt = fmt.replace(
|
||||||
|
ret[1],
|
||||||
|
ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, "0"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Logger {
|
||||||
|
static log(...args) {
|
||||||
|
console.log(...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// red
|
||||||
|
static error(...args) {
|
||||||
|
console.error("\u001b[31m [ERROR]", ...args, "\u001b[0m");
|
||||||
|
}
|
||||||
|
|
||||||
|
// yellow
|
||||||
|
static warn(...args) {
|
||||||
|
console.warn("\u001b[33m [WARN]", ...args, "\u001b[0m");
|
||||||
|
}
|
||||||
|
|
||||||
|
// blue
|
||||||
|
static debug(...args) {
|
||||||
|
console.log("\u001b[34m [DEBUG]\u001b[0m", ...args);
|
||||||
|
}
|
||||||
|
|
||||||
|
// green
|
||||||
|
static info(...args) {
|
||||||
|
console.log("\u001b[32m [INFO]", ...args, "\u001b[0m");
|
||||||
|
}
|
||||||
|
|
||||||
|
// cyan
|
||||||
|
static trace(...args) {
|
||||||
|
console.log("\u001b[36m [TRACE]\u001b[0m", ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isRunning(query, cb) {
|
||||||
|
let platform = process.platform;
|
||||||
|
let cmd = "";
|
||||||
|
switch (platform) {
|
||||||
|
case "win32":
|
||||||
|
cmd = `tasklist`;
|
||||||
|
break;
|
||||||
|
case "darwin":
|
||||||
|
cmd = `ps -ax | grep ${query}`;
|
||||||
|
break;
|
||||||
|
case "linux":
|
||||||
|
cmd = `ps -A`;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
exec(cmd, (err, stdout, stderr) => {
|
||||||
|
cb(stdout.toLowerCase().indexOf(query.toLowerCase()) > -1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"usage": "Copy and rename this file to zotero-cmd.json. Edit the cmd.",
|
"usage": "Copy and rename this file to zotero-cmd.json. Edit the cmd.",
|
||||||
"killZoteroWindows": "taskkill /f /im zotero.exe",
|
"killZoteroWindows": "taskkill /f /im zotero.exe",
|
||||||
"killZoteroUnix": "kill -9 $(ps -x | grep zotero)",
|
"killZoteroUnix": "kill -9 $(ps -x | grep '[z]otero' | awk '{print $1}')",
|
||||||
"exec": {
|
"exec": {
|
||||||
"@comment-zoteroBinPath": "Please input the path of the Zotero binary file in `zoteroBinPath`.",
|
"@comment-zoteroBinPath": "Please input the path of the Zotero binary file in `zoteroBinPath`.",
|
||||||
"@comment-zoteroBinPath-tip": "The path delimiter should be escaped as `\\` for win32. The path is `*/Zotero.app/Contents/MacOS/zotero` for MacOS.",
|
"@comment-zoteroBinPath-tip": "The path delimiter should be escaped as `\\` for win32. The path is `*/Zotero.app/Contents/MacOS/zotero` for MacOS.",
|
||||||
25
src/hooks.ts
25
src/hooks.ts
|
|
@ -12,6 +12,14 @@ async function onStartup() {
|
||||||
Zotero.unlockPromise,
|
Zotero.unlockPromise,
|
||||||
Zotero.uiReadyPromise,
|
Zotero.uiReadyPromise,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// TODO: Remove this after zotero#3387 is merged
|
||||||
|
if (__env__ === "development") {
|
||||||
|
// Keep in sync with the scripts/startup.mjs
|
||||||
|
const loadDevToolWhen = `Plugin ${config.addonID} startup`;
|
||||||
|
ztoolkit.log(loadDevToolWhen);
|
||||||
|
}
|
||||||
|
|
||||||
initLocale();
|
initLocale();
|
||||||
|
|
||||||
await tldrs.getAsync();
|
await tldrs.getAsync();
|
||||||
|
|
@ -25,8 +33,6 @@ async function onMainWindowLoad(win: Window): Promise<void> {
|
||||||
// Create ztoolkit for every window
|
// Create ztoolkit for every window
|
||||||
addon.data.ztoolkit = createZToolkit();
|
addon.data.ztoolkit = createZToolkit();
|
||||||
|
|
||||||
await Zotero.Promise.delay(1000);
|
|
||||||
|
|
||||||
UIFactory.registerRightClickMenuItem();
|
UIFactory.registerRightClickMenuItem();
|
||||||
|
|
||||||
UIFactory.registerRightClickCollectionMenuItem();
|
UIFactory.registerRightClickCollectionMenuItem();
|
||||||
|
|
@ -158,11 +164,9 @@ function onUpdateItems(items: Zotero.Item[], forceFetch: boolean = false) {
|
||||||
ztoolkit.ItemBox.refresh();
|
ztoolkit.ItemBox.refresh();
|
||||||
popupWin.changeLine({
|
popupWin.changeLine({
|
||||||
progress: (index * 100) / count,
|
progress: (index * 100) / count,
|
||||||
text: `${getString("popWindow-waiting")}: ${
|
text: `${getString("popWindow-waiting")}: ${count - index - 1
|
||||||
count - index - 1
|
}; ${getString("popWindow-succeed")}: ${succeedItems.length
|
||||||
}; ${getString("popWindow-succeed")}: ${
|
}; ${getString("popWindow-failed")}: ${failedItems.length}`,
|
||||||
succeedItems.length
|
|
||||||
}; ${getString("popWindow-failed")}: ${failedItems.length}`,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
@ -171,9 +175,8 @@ function onUpdateItems(items: Zotero.Item[], forceFetch: boolean = false) {
|
||||||
popupWin.changeLine({
|
popupWin.changeLine({
|
||||||
type: "success",
|
type: "success",
|
||||||
progress: 100,
|
progress: 100,
|
||||||
text: `${getString("popWindow-succeed")}: ${
|
text: `${getString("popWindow-succeed")}: ${succeedItems.length
|
||||||
succeedItems.length
|
}; ${getString("popWindow-failed")}: ${failedItems.length}`,
|
||||||
}; ${getString("popWindow-failed")}: ${failedItems.length}`,
|
|
||||||
});
|
});
|
||||||
popupWin.startCloseTimer(3000);
|
popupWin.startCloseTimer(3000);
|
||||||
})();
|
})();
|
||||||
|
|
@ -182,7 +185,7 @@ function onUpdateItems(items: Zotero.Item[], forceFetch: boolean = false) {
|
||||||
|
|
||||||
// Add your hooks here. For element click, etc.
|
// Add your hooks here. For element click, etc.
|
||||||
// Keep in mind hooks only do dispatch. Don't add code that does real jobs in hooks.
|
// Keep in mind hooks only do dispatch. Don't add code that does real jobs in hooks.
|
||||||
// Otherwise the code would be hard to read and maintian.
|
// Otherwise the code would be hard to read and maintain.
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
onStartup,
|
onStartup,
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,6 @@ import { config } from "../package.json";
|
||||||
const basicTool = new BasicTool();
|
const basicTool = new BasicTool();
|
||||||
|
|
||||||
if (!basicTool.getGlobal("Zotero")[config.addonInstance]) {
|
if (!basicTool.getGlobal("Zotero")[config.addonInstance]) {
|
||||||
// Set global variables
|
|
||||||
_globalThis.Zotero = basicTool.getGlobal("Zotero");
|
|
||||||
defineGlobal("window");
|
defineGlobal("window");
|
||||||
defineGlobal("document");
|
defineGlobal("document");
|
||||||
defineGlobal("ZoteroPane");
|
defineGlobal("ZoteroPane");
|
||||||
|
|
@ -16,8 +14,6 @@ if (!basicTool.getGlobal("Zotero")[config.addonInstance]) {
|
||||||
return _globalThis.addon.data.ztoolkit;
|
return _globalThis.addon.data.ztoolkit;
|
||||||
});
|
});
|
||||||
Zotero[config.addonInstance] = addon;
|
Zotero[config.addonInstance] = addon;
|
||||||
// Trigger addon hook for initialization
|
|
||||||
addon.hooks.onStartup();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function defineGlobal(name: Parameters<BasicTool["getGlobal"]>[0]): void;
|
function defineGlobal(name: Parameters<BasicTool["getGlobal"]>[0]): void;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue