feat: publish development scripts as independent npm packages (#109)

* feat!: use zotero-plugin-scaffold

* fix: tsc error

* docs: update readme and comment

* fix: clean code, remove useless file

* fix: update zotero-plugin-scaffold

* fix: set esbuild target to ff115

* chore: clean and update deps

* fix: Simplify config files and update dep

* fix: update deps

* chore: update renovate config

* fix: default use proxy mode to install plugin

* chore: bump scaffold to 0.0.19

resolve: #113

* fix: bump scaffold to 0.0.20

* fix: use new devtools hack
This commit is contained in:
Northword 2024-05-15 15:33:42 +08:00 committed by GitHub
parent 8b1c82681d
commit 0d76086064
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 179 additions and 852 deletions

29
.env.example Normal file
View File

@ -0,0 +1,29 @@
# Usage:
# Copy this file as `.env` and fill in the variables below as instructed.
# If you are developing more than one plugin, you can store the bin path and
# profile path in the system environment variables, which can be omitted here.
# The path of the Zotero binary file.
# The path delimiter should be escaped as `\\` for win32.
# The path is `*/Zotero.app/Contents/MacOS/zotero` for MacOS.
ZOTERO_PLUGIN_ZOTERO_BIN_PATH = /path/to/zotero.exe
# The path of the profile used for development.
# Start the profile manager by `/path/to/zotero.exe -p` to create a profile for development.
# @see https://www.zotero.org/support/kb/profile_directory
ZOTERO_PLUGIN_PROFILE_PATH = /path/to/profile
# The directory where the database is located.
# If this field is kept empty, Zotero will start with the default data.
# @see https://www.zotero.org/support/zotero_data
ZOTERO_PLUGIN_DATA_DIR =
# Custom commands to kill Zotero processes.
# Commands for different platforms are already built into zotero-plugin,
# if the built-in commands are not suitable for your needs, please modify this variable.
# ZOTERO_PLUGIN_KILL_COMMAND =
# GitHub Token
# For release-it auto create release and upload assets
# GITHUB_TOKEN =

11
.github/renovate.json vendored
View File

@ -7,11 +7,18 @@
":prConcurrentLimitNone",
":enableVulnerabilityAlerts",
":dependencyDashboard",
"schedule:weekends"
"group:allNonMajor",
"schedule:weekly"
],
"labels": ["dependencies"],
"packageRules": [
{
"matchPackageNames": ["zotero-plugin-toolkit", "zotero-types"],
"matchPackageNames": [
"zotero-plugin-toolkit",
"zotero-types",
"zotero-plugin-scaffold"
],
"schedule": ["at any time"],
"automerge": true
}
],

View File

@ -29,9 +29,19 @@ jobs:
- name: Install deps
run: npm install
- name: Build
run: |
npm run build
- name: Release to GitHub
run: |
npm run release -- --no-increment --no-git --github.release --ci --VV
npm run release
# cp build/update.json update.json
# cp build/update-beta.json update-beta.json
# git add update.json
# git add update-beta.json
# git commit -m 'chore(publish): synchronizing `update.json`'
# git push
sleep 1s
- name: Notify release

3
.gitignore vendored
View File

@ -5,4 +5,5 @@ package-lock.json
pnpm-lock.yaml
yarn.lock
zotero-cmd.json
.DS_Store
.DS_Store
.env

View File

@ -65,12 +65,9 @@ If you are using this repo, I recommended that you put the following badge on yo
- Plugin develop/build/release workflow:
- Automatically generate/update plugin id/version, update configrations, and set environment variables (`development` / `production`);
- Automatically build and reload code in Zotero;
- Automatically release to GitHub (using [release-it](https://github.com/release-it/release-it));
- Automatically release to GitHub;
- Prettier and ES Lint integration.
> [!warning]
> The localization system is upgraded (`dtd` is deprecated and we do not use `.properties` anymore). Only supports Zotero 7.0.0-beta.12 or higher now. If you want to support Zotero 6, you may need to use `dtd`, `properties`, and `ftl` at the same time. See the staled branch `zotero6-bootstrap`.
## Examples
This repo provides examples for [zotero-plugin-toolkit](https://github.com/windingwind/zotero-plugin-toolkit) APIs.
@ -178,8 +175,6 @@ Activate with `Shift+P`.
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
},
}
```
@ -187,18 +182,20 @@ Activate with `Shift+P`.
> [!warning]
> Be careful to set the addonID and addonRef to avoid conflict.
If you need to host your XPI packages outside of GitHub, remove `releasePage` and add `updateLink` with the value set to your XPI download URL.
If you need to host your XPI packages outside of GitHub, moidify `updateURL` and add `xpiDownloadLink` in `zotero-plugin.config.ts`.
2. Copy zotero command line config file. Modify the commands that starts your installation of the beta Zotero.
2. Copy the environment variable file. Modify the commands that starts your installation of the beta Zotero.
> (Optional) Do this only once: Start the beta Zotero with `/path/to/zotero -p`. Create a new profile and use it as your development profile.
> Put the path of the profile into the `profilePath` in `zotero-cmd.json` to specify which profile to use.
> Create a development profile (Optional)
> Start the beta Zotero with `/path/to/zotero -p`. Create a new profile and use it as your development profile. Do this only once
```sh
cp ./scripts/zotero-cmd-template.json ./scripts/zotero-cmd.json
vim ./scripts/zotero-cmd.json
cp .env.example .env
vim .env
```
If you are developing more than one plugin, you can store the bin path and profile path in the system environment variables, which can be omitted here.
3. Install dependencies with `npm install`
> If you are using `pnpm` as the package manager for your project, you need to add `public-hoist-pattern[]=*@types/bluebird*` to `.npmrc`, see <https://github.com/windingwind/zotero-types?tab=readme-ov-file#usage>.
@ -209,7 +206,6 @@ Start development server with `npm start`, it will:
- Prebuild the plugin in development mode
- Start Zotero with plugin loaded from `build/`
- Open devtool
- Watch `src/**` and `addon/**`.
- If `src/**` changed, run esbuild and reload
- If `addon/**` has changed, rebuild the plugin (in development mode) and reload
@ -226,10 +222,7 @@ When file changes are detected in `src` or `addon`, the plugin will be automatic
<details style="text-indent: 2em">
<summary>💡 Steps to add this feature to an existing plugin</summary>
1. Copy `scripts/**.mjs`
2. Copy `server`, `build`, and `stop` commands in `package.json`
3. Run `npm install --save-dev chokidar`
4. Done.
Please see [zotero-plugin-scaffold](https://github.com/northword/zotero-plugin-scaffold).
</details>
@ -246,7 +239,7 @@ You can also:
Run `npm run build` to build the plugin in production mode, and the xpi for installation and the built code is under `build` folder.
Steps in `scripts/build.mjs`:
Steps of build:
- Create/empty `build/`.
- Copy `addon/**` to `build/addon/**`
@ -254,7 +247,7 @@ Steps in `scripts/build.mjs`:
- Prepare locale files to [avoid conflict](https://www.zotero.org/support/dev/zotero_7_for_developers#avoiding_localization_conflicts)
- Rename `**/*.flt` to `**/${addonRef}-*.flt`
- Prefix each fluent message with `addonRef-`
- Use Esbuild to build `.ts` source code to `.js`, build `src/index.ts` to `./build/addon/chrome/content/scripts`.
- Use ESBuild to build `.ts` source code to `.js`, build `src/index.ts` to `./build/addon/chrome/content/scripts`.
- (Production mode only) Zip the `./build/addon` to `./build/*.xpi`
- (Production mode only) Prepare `update.json` or `update-beta.json`
@ -271,15 +264,14 @@ Steps in `scripts/build.mjs`:
To build and release, use
```shell
# A release-it command: version increase, npm run build, git push, and GitHub release
# release-it: https://github.com/release-it/release-it
# version increase, git add, commit and push
# then on ci, npm run build, and release to GitHub
npm run release
```
> [!note]
> In this template, release-it is configured to locally bump the version, build, and push commits and git.tags, subsequently GitHub Action will rebuild the plugin and publish the XPI to GitHub Release.
>
> If you need to release a locally built XPI, set `release-it.github.release` to `true` in `package.json` and remove `.github/workflows/release.yml`. Besides that, you need to set the environment variable `GITHUB_TOKEN`, get it in <https://github.com/settings/tokens>.
#### About Prerelease

View File

@ -57,18 +57,15 @@
- 事件驱动、函数式编程的可扩展框架;
- 简单易用,开箱即用;
- ⭐[新特性!]自动热重载!每当修改源码时,都会自动编译并重新加载插件;[详情请跳转→](#自动热重载)
- `src/modules/examples.ts` 中有丰富的示例涵盖了插件中常用的大部分API (使用的插件工具包 zotero-plugin-toolkit仓库地址 https://github.com/windingwind/zotero-plugin-toolkit)
- `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)
- 为使用 JavaScript 编写的 Zotero 源码提供全面的类型定义支持 (使用 [zotero-types](https://github.com/windingwind/zotero-types))
- 全局变量和环境设置;
- 插件开发/构建/发布工作流:
- 自动生成/更新插件id和版本、更新配置和设置环境变量 (`development`/`production`)
- 自动生成/更新插件版本、更新配置和设置环境变量 (`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` 分支.
- 自动发布到 GitHub ;
- 集成 Prettier 和 ES Lint;
## Examples 示例
@ -133,15 +130,15 @@ Obsidian风格的指令输入模块它通过接受文本来运行插件
- registerAlertPromptExample
## Quick Start Guide 快速入门指南
## 快速上手
### 0 前置要求(Requirement)
### 0 环境要求
1. 安装测试版 Zoterohttps://www.zotero.org/support/beta_builds
2. 安装 Node.jshttps://nodejs.org/en/)和 Githttps://git-scm.com/
1. 安装 [beta 版 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.
> 本指南假定你已经对 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)
@ -167,18 +164,16 @@ Obsidian风格的指令输入模块它通过接受文本来运行插件
```json5
{
version: "", // to 0.0.0
version: "", // 修改为 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
addonName: "", // 插件名称
addonID: "", // 插件 ID 【重要:防止冲突】
addonRef: "", // 插件命名空间:元素前缀等
addonInstance: "", // 注册在 Zotero 根下的实例名
prefsPrefix: "extensions.zotero.${addonRef}", // 首选项的前缀
},
}
```
@ -186,23 +181,26 @@ Obsidian风格的指令输入模块它通过接受文本来运行插件
> [!warning]
> 注意设置 addonID 和 addonRef 以避免冲突.
如果你需要在GitHub以外的地方托管你的 XPI 包,请删除 `releasePage` 并添加 `updateLink`,并将值设置为你的 XPI 下载地址.
如果你需要在 GitHub 以外的地方托管你的 XPI 包,请修改 `zotero-plugin.config.ts` 中的 `updateURL``xpiDownloadLink`
2. 复制 Zotero 启动配置,填入 Zotero 可执行文件路径和 profile 路径.
> (可选项) 此操作仅需执行一次: 使用 `/path/to/zotero -p` 启动 Zotero创建一个新的配置文件并用作开发配置文件.
> 将配置文件的路径 `profilePath` 放入 `zotero-cmd.json` 中,以指定要使用的配置文件.
> (可选项) 创建开发用 profile 目录:
>
> 此操作仅需执行一次: 使用 `/path/to/zotero -p` 启动 Zotero创建一个新的配置文件并用作开发配置文件。
```sh
cp ./scripts/zotero-cmd-template.json ./scripts/zotero-cmd.json
vim ./scripts/zotero-cmd.json
cp .env.example .env
vim .env
```
如果你维护了多个插件,可以将这些内容存入系统环境变量,以避免在每个插件中都需要重复设置。
3. 运行 `npm install` 以安装相关依赖
> 如果你使用 `pnpm` 作为包管理器,你需要添加 `public-hoist-pattern[]=*@types/bluebird*``.npmrc`, 详情请查看 zotero-typeshttps://github.com/windingwind/zotero-types?tab=readme-ov-file#usage)的文档.
> 如果你使用 `pnpm` 作为包管理器,你需要添加 `public-hoist-pattern[]=*@types/bluebird*``.npmrc`, 详情请查看 zotero-types<https://github.com/windingwind/zotero-types?tab=readme-ov-file#usage)的文档>.
### 3 开始开发(Coding)
### 3 开发插件
使用 `npm start` 启动开发服务器,它将:
@ -225,14 +223,11 @@ Obsidian风格的指令输入模块它通过接受文本来运行插件
<details style="text-indent: 2em">
<summary>💡 将此功能添加到现有插件的步骤</summary>
1. 复制 `scripts/**.mjs`
2. 复制 `server``build``stop` 命令到 `package.json`
3. 运行 `npm install --save-dev chokidar`
4. 结束.
请参阅:[zotero-plugin-scaffold](https://github.com/northword/zotero-plugin-scaffold)。
</details>
#### 在 Zotero 中 Debug
#### 调试代码
你还可以:
@ -244,16 +239,16 @@ Obsidian风格的指令输入模块它通过接受文本来运行插件
> XUL 文档: <http://www.devdoc.net/web/developer.mozilla.org/en-US/docs/XUL.html>
### 4 构建(Build)
### 4 构建插件
运行 `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
- 准备本地化文件以避免冲突,查看官方文档了解更多(<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`
@ -268,27 +263,25 @@ Obsidian风格的指令输入模块它通过接受文本来运行插件
> - 你可以根据此变量决定用户无法查看/使用的内容.
> - 在生产模式下,构建脚本将自动打包插件并更新 `update.json`.
### 5 发布(Release)
### 5 发布
如果要构建和发布插件,运行如下指令:
```shell
# A release-it command: version increase, npm run build, git push, and GitHub release
# release-it: https://github.com/release-it/release-it
# version increase, git add, commit and push
# then on ci, npm run build, and release to GitHub
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 Tokenhttps://github.com/settings/tokens.
> 在此模板中release-it 被配置为在本地更新版本号、提交并推送标签,随后 GitHub Action 将重新构建插件并将 XPI 发布到 GitHub Release.
#### 关于预发布
该模板将 `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)获取.
> 严格来说,区分 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 更多细节
@ -333,7 +326,7 @@ createElement(document, "button", { namespace: "xul" }); // manually set namespa
### 关于 Zotero API(About Zotero API)
Zotero 文档已过时且不完整,克隆 https://github.com/zotero/zotero 并全局搜索关键字.
Zotero 文档已过时且不完整,克隆 <https://github.com/zotero/zotero> 并全局搜索关键字.
> ⭐[zotero-types](https://github.com/windingwind/zotero-types) 提供了最常用的 Zotero API在默认情况下它被包含在此模板中. 你的 IDE 将为大多数的 API 提供提醒.

View File

@ -7,47 +7,39 @@
"addonID": "addontemplate@euclpts.com",
"addonRef": "addontemplate",
"addonInstance": "AddonTemplate",
"prefsPrefix": "extensions.zotero.addontemplate",
"releasePage": "https://github.com/windingwind/zotero-addon-template/releases",
"updateJSON": "https://raw.githubusercontent.com/windingwind/zotero-addon-template/main/update.json"
},
"main": "src/index.ts",
"scripts": {
"start": "node scripts/server.mjs",
"build": "tsc --noEmit && node scripts/build.mjs production",
"stop": "node scripts/stop.mjs",
"lint": "prettier --write . && eslint . --ext .ts --fix",
"test": "echo \"Error: no test specified\" && exit 1",
"release": "release-it --only-version --preReleaseId=beta",
"update-deps": "npm update --save"
"prefsPrefix": "extensions.zotero.addontemplate"
},
"repository": {
"type": "git",
"url": "git+https://github.com/windingwind/zotero-addon-template.git"
},
"author": "windingwind",
"license": "AGPL-3.0-or-later",
"bugs": {
"url": "https://github.com/windingwind/zotero-addon-template/issues"
},
"homepage": "https://github.com/windingwind/zotero-addon-template#readme",
"license": "AGPL-3.0-or-later",
"scripts": {
"start": "zotero-plugin serve",
"build": "tsc --noEmit && zotero-plugin build",
"lint": "prettier --write . && eslint . --ext .ts --fix",
"release": "zotero-plugin release",
"test": "echo \"Error: no test specified\" && exit 1",
"update-deps": "npm update --save"
},
"dependencies": {
"zotero-plugin-toolkit": "^2.3.30"
"zotero-plugin-toolkit": "^2.3.31"
},
"devDependencies": {
"@types/node": "^20.12.8",
"@typescript-eslint/eslint-plugin": "^7.8.0",
"@typescript-eslint/parser": "^7.8.0",
"chokidar": "^3.6.0",
"compressing": "^1.10.0",
"esbuild": "^0.20.2",
"@types/node": "^20.12.12",
"@typescript-eslint/eslint-plugin": "^7.9.0",
"@typescript-eslint/parser": "^7.9.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"prettier": "^3.2.5",
"release-it": "^17.2.1",
"replace-in-file": "^7.1.0",
"typescript": "^5.4.5",
"zotero-types": "^2.0.0"
"zotero-plugin-scaffold": "^0.0.21",
"zotero-types": "^1.3.24"
},
"eslintConfig": {
"env": {
@ -113,23 +105,5 @@
}
}
]
},
"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"
}
}
}

View File

@ -1,243 +0,0 @@
import details from "../package.json" with { type: "json" };
import {
Logger,
clearFolder,
copyFileSync,
copyFolderRecursiveSync,
dateFormat,
} from "./utils.mjs";
import { zip } from "compressing";
import { build } from "esbuild";
import { existsSync, readdirSync, renameSync } from "fs";
import path from "path";
import { env, exit } from "process";
import replaceInFile from "replace-in-file";
const { replaceInFileSync } = replaceInFile;
process.env.NODE_ENV =
process.argv[2] === "production" ? "production" : "development";
const buildDir = "build";
const { name, author, description, homepage, version, config } = details;
const isPreRelease = version.includes("-");
function replaceString(buildTime) {
const replaceFrom = [
/__author__/g,
/__description__/g,
/__homepage__/g,
/__buildVersion__/g,
/__buildTime__/g,
];
const replaceTo = [author, description, homepage, version, buildTime];
config.updateURL = isPreRelease
? config.updateJSON.replace("update.json", "update-beta.json")
: config.updateJSON;
replaceFrom.push(
...Object.keys(config).map((k) => new RegExp(`__${k}__`, "g")),
);
replaceTo.push(...Object.values(config));
const replaceResult = replaceInFileSync({
files: [
`${buildDir}/addon/**/*.xhtml`,
`${buildDir}/addon/**/*.html`,
`${buildDir}/addon/**/*.css`,
`${buildDir}/addon/**/*.json`,
`${buildDir}/addon/prefs.js`,
`${buildDir}/addon/manifest.json`,
`${buildDir}/addon/bootstrap.js`,
],
from: replaceFrom,
to: replaceTo,
countMatches: true,
});
// Logger.debug(
// "[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) => {
const matchs = [...input.matchAll(/(data-l10n-id)="(\S*)"/g)];
matchs.map((match) => {
input = input.replace(
match[0],
`${match[1]}="${config.addonRef}-${match[2]}"`,
);
MessagesInHTML.add(match[2]);
});
return input;
},
});
// Walk the sub folders of `build/addon/locale`
const localesPath = path.join(buildDir, "addon/locale"),
localeNames = readdirSync(localesPath, { withFileTypes: true })
.filter((dirent) => dirent.isDirectory())
.map((dirent) => dirent.name);
for (const localeName of localeNames) {
const localePath = path.join(localesPath, localeName);
const ftlFiles = readdirSync(localePath, {
withFileTypes: true,
})
.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}`);
}
});
}
}
function prepareUpdateJson() {
// If it is a pre-release, use update-beta.json
if (!isPreRelease) {
copyFileSync("scripts/update-template.json", "update.json");
}
if (existsSync("update-beta.json") || isPreRelease) {
copyFileSync("scripts/update-template.json", "update-beta.json");
}
const updateLink =
config.updateLink ?? isPreRelease
? `${config.releasePage}/download/v${version}/${name}.xpi`
: `${config.releasePage}/latest/download/${name}.xpi`;
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}`),
);
}
export const esbuildOptions = {
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=${[
env.NODE_ENV,
]}`,
);
clearFolder(buildDir);
copyFolderRecursiveSync("addon", buildDir);
Logger.debug("[Build] Replacing");
replaceString(buildTime);
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.`,
);
}
}
if (process.env.NODE_ENV === "production") {
main().catch((err) => {
Logger.error(err);
exit(1);
});
}

View File

@ -1,75 +0,0 @@
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);
}
})()`;

View File

@ -1,87 +0,0 @@
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);
});

View File

@ -1,119 +0,0 @@
import details from "../package.json" assert { type: "json" };
import { Logger } from "./utils.mjs";
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 { 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)) {
throw new Error("Zotero binary does not exist.");
}
if (!existsSync(profilePath)) {
throw new Error("The given Zotero profile does not exist.");
}
function prepareDevEnv() {
const addonProxyFilePath = path.join(profilePath, `extensions/${addonID}`);
const buildPath = path.resolve("build/addon");
function writeAddonProxyFile() {
writeFileSync(addonProxyFilePath, buildPath);
Logger.debug(
`Addon proxy file has been updated.
File path: ${addonProxyFilePath}
Addon path: ${buildPath} `,
);
}
if (existsSync(addonProxyFilePath)) {
if (readFileSync(addonProxyFilePath, "utf-8") !== buildPath) {
writeAddonProxyFile();
}
} else {
writeAddonProxyFile();
}
const addonXpiFilePath = path.join(profilePath, `extensions/${addonID}.xpi`);
if (existsSync(addonXpiFilePath)) {
rmSync(addonXpiFilePath);
}
const prefsPath = path.join(profilePath, "prefs.js");
if (existsSync(prefsPath)) {
const PrefsLines = readFileSync(prefsPath, "utf-8").split("\n");
const filteredLines = PrefsLines.map((line) => {
if (
line.includes("extensions.lastAppBuildId") ||
line.includes("extensions.lastAppVersion")
) {
return;
}
if (line.includes("extensions.zotero.dataDir") && dataDir !== "") {
return `user_pref("extensions.zotero.dataDir", "${dataDir.replace(/\\\\?/g, "\\\\")}");`;
}
return line;
});
const updatedPrefs = filteredLines.join("\n");
writeFileSync(prefsPath, updatedPrefs, "utf-8");
Logger.debug("The <profile>/prefs.js has been modified.");
}
}
function prepareLog() {
clearFolder(logPath);
writeFileSync(logFilePath, "");
}
export function main(callback) {
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();
});
}

View File

@ -1,26 +0,0 @@
import { Logger, isRunning } from "./utils.mjs";
import cmd from "./zotero-cmd.json" assert { type: "json" };
import { execSync } from "child_process";
import process from "process";
const { killZoteroWindows, killZoteroUnix } = cmd;
isRunning("zotero", (status) => {
if (status) {
killZotero();
} else {
Logger.warn("No Zotero running.");
}
});
function killZotero() {
try {
if (process.platform === "win32") {
execSync(killZoteroWindows);
} else {
execSync(killZoteroUnix);
}
} catch (e) {
Logger.error(e);
}
}

View File

@ -1,17 +0,0 @@
{
"addons": {
"__addonID__": {
"updates": [
{
"version": "__buildVersion__",
"update_link": "__updateLink__",
"applications": {
"zotero": {
"strict_min_version": "6.999"
}
}
}
]
}
}
}

View File

@ -1,129 +0,0 @@
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);
});
}

View File

@ -1,20 +0,0 @@
{
"usage": "Copy and rename this file to zotero-cmd.json. Edit the cmd.",
"killZoteroWindows": "taskkill /f /im zotero.exe",
"killZoteroUnix": "kill -9 $(ps -x | grep '[z]otero' | awk '{print $1}')",
"exec": {
"@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.",
"zoteroBinPath": "/path/to/zotero.exe",
"@comment-profilePath": "Please input the path of the profile used for development in `profilePath`.",
"@comment-profilePath-tip": "Start the profile manager by `/path/to/zotero.exe -p` to create a profile for development",
"@comment-profilePath-see": "https://www.zotero.org/support/kb/profile_directory",
"profilePath": "/path/to/profile",
"@comment-dataDir": "Please input the directory where the database is located in dataDir",
"@comment-dataDir-tip": "If this field is kept empty, Zotero will start with the default data.",
"@comment-dataDir-see": "https://www.zotero.org/support/zotero_data",
"dataDir": ""
}
}

View File

@ -17,13 +17,6 @@ async function onStartup() {
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();
BasicExampleFactory.registerPrefs();

View File

@ -1,17 +0,0 @@
{
"addons": {
"addontemplate@euclpts.com": {
"updates": [
{
"version": "1.1.2",
"update_link": "https://github.com/windingwind/zotero-addon-template/releases/latest/download/zotero-addon-template.xpi",
"applications": {
"zotero": {
"strict_min_version": "6.999"
}
}
}
]
}
}
}

61
zotero-plugin.config.ts Normal file
View File

@ -0,0 +1,61 @@
import { defineConfig } from "zotero-plugin-scaffold";
import pkg from "./package.json";
import { copyFileSync } from "fs";
export default defineConfig({
source: ["src", "addon"],
dist: "build",
name: pkg.config.addonName,
id: pkg.config.addonID,
namespace: pkg.config.addonRef,
updateURL: `https://github.com/{{owner}}/{{repo}}/releases/download/release/${
pkg.version.includes("-") ? "update-beta.json" : "update.json"
}`,
xpiDownloadLink:
"https://github.com/{{owner}}/{{repo}}/releases/download/v{{version}}/{{xpiName}}.xpi",
server: {
asProxy: true,
},
build: {
assets: ["addon/**/*.*"],
define: {
...pkg.config,
author: pkg.author,
description: pkg.description,
homepage: pkg.homepage,
buildVersion: pkg.version,
buildTime: "{{buildTime}}",
},
esbuildOptions: [
{
entryPoints: ["src/index.ts"],
define: {
__env__: `"${process.env.NODE_ENV}"`,
},
bundle: true,
target: "firefox115",
outfile: `build/addon/chrome/content/scripts/${pkg.config.addonRef}.js`,
},
],
// If you want to checkout update.json into the repository, uncomment the following lines:
// makeUpdateJson: {
// hash: false,
// },
// hooks: {
// "build:makeUpdateJSON": (ctx) => {
// copyFileSync("build/update.json", "update.json");
// copyFileSync("build/update-beta.json", "update-beta.json");
// },
// },
},
// release: {
// bumpp: {
// execute: "npm run build",
// },
// },
// If you need to see a more detailed build log, uncomment the following line:
// logLevel: "trace",
});