From fb9109d87d06676c1c4be07907b9d6adc9387ed3 Mon Sep 17 00:00:00 2001 From: Northword <44738481+northword@users.noreply.github.com> Date: Tue, 12 Dec 2023 15:55:00 +0800 Subject: [PATCH] refactor: rebuild development scripts and release workflow (#84) * refactor: add dev server and fix `update-beta.json` bug * chore: add renovate config * feat: release plugin to GitHub release via action * chore: update vsc settings * Cancel release by action * docs: update readme * Update README.md * Update README.md fix format * Update README.md * Update README.md fix hint * tweak * Merge #81 * Fix indent * Merge https://github.com/windingwind/zotero-plugin-template/commit/ade49628ff10d5f6e39b5fd205165d65ec9e3fb1 * Revent delete env * tweak * feat: release via GitHub action * docs: update readme * Update README.md * style: fix prettier * add: stdout log to file * Update README.md fix typo * Update release.yml fix typo * write Zotero log to `logs/zotero.log` * do not provide update-beta.json default * fix prettier in readme * tweak * fix wrong link in readme * Move 3rd package config to package.json --------- Co-authored-by: windingwind <33902321+windingwind@users.noreply.github.com> --- .eslintrc.json | 41 --- .github/renovate.json | 21 ++ .github/workflows/release.yml | 33 ++ .gitignore | 5 +- .prettierignore | 7 + .prettierrc | 3 - .release-it.json | 14 - .vscode/launch.json | 15 +- .vscode/settings.json | 2 +- README.md | 261 +++++++------- package.json | 105 +++++- scripts/build.mjs | 318 ++++++++---------- scripts/reload.mjs | 34 -- scripts/scripts.mjs | 75 +++++ scripts/server.mjs | 90 +++++ scripts/start.mjs | 85 +++-- scripts/stop.mjs | 28 +- .../update-template.json | 0 scripts/utils.mjs | 129 +++++++ src/index.ts | 47 --- update-beta.json | 17 - 21 files changed, 810 insertions(+), 520 deletions(-) delete mode 100644 .eslintrc.json create mode 100644 .github/renovate.json create mode 100644 .github/workflows/release.yml create mode 100644 .prettierignore delete mode 100644 .prettierrc delete mode 100644 .release-it.json delete mode 100644 scripts/reload.mjs create mode 100644 scripts/scripts.mjs create mode 100644 scripts/server.mjs rename update-template.json => scripts/update-template.json (100%) create mode 100644 scripts/utils.mjs delete mode 100644 update-beta.json diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index bd779ad..0000000 --- a/.eslintrc.json +++ /dev/null @@ -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" - ] -} diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..1289413 --- /dev/null +++ b/.github/renovate.json @@ -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 + } +} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..b533acf --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,33 @@ +name: Release + +on: + push: + tags: + - v** + +permissions: + contents: write + +jobs: + release-it: + runs-on: ubuntu-latest + env: + GITHUB_TOKEN: ${{ secrets.GitHub_TOKEN }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: 20 + # cache: npm + + - name: Install deps + run: npm install + + - name: Release to GitHub + # if: github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref, 'refs/tags/v') + run: | + npm run release -- --no-increment --no-git --github.release --ci --verbose diff --git a/.gitignore b/.gitignore index d277962..6dfac6b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ -**/build +build +logs node_modules package-lock.json +pnpm-lock.yaml +yarn.lock zotero-cmd.json \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..153e309 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,7 @@ +build +logs +node_modules +package-lock.json +yarn.lock +pnpm-lock.yaml +# zotero-cmd.json diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 75fa134..0000000 --- a/.prettierrc +++ /dev/null @@ -1,3 +0,0 @@ -{ - "tabWidth": 2 -} diff --git a/.release-it.json b/.release-it.json deleted file mode 100644 index 69da605..0000000 --- a/.release-it.json +++ /dev/null @@ -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}." - } -} diff --git a/.vscode/launch.json b/.vscode/launch.json index 1663cf3..8b7f88b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,23 +7,16 @@ { "type": "node", "request": "launch", - "name": "StartDev", + "name": "Start", "runtimeExecutable": "npm", - "runtimeArgs": ["run", "start-watch"] + "runtimeArgs": ["run", "start"] }, { "type": "node", "request": "launch", - "name": "Restart", + "name": "Build", "runtimeExecutable": "npm", - "runtimeArgs": ["run", "restart"] - }, - { - "type": "node", - "request": "launch", - "name": "Restart in Prod Mode", - "runtimeExecutable": "npm", - "runtimeArgs": ["run", "restart-prod"] + "runtimeArgs": ["run", "build"] } ] } diff --git a/.vscode/settings.json b/.vscode/settings.json index be8156d..ee7b675 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,6 +2,6 @@ "editor.formatOnType": false, "editor.formatOnSave": true, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" } } diff --git a/README.md b/README.md index 692f38d..811d9a7 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,22 @@ This is a plugin template for [Zotero](https://www.zotero.org/). [English](README.md) | [็ฎ€ไฝ“ไธญๆ–‡](doc/README-zhCN.md) -Some plugins created with this template: +[๐Ÿ“– Plugin Development Documentation](https://zotero.yuque.com/books/share/8d230829-6004-4934-b4c6-685a7001bfa0/vec88d) (Chinese, outdated) + +[๐Ÿ“– Plugin Development Documentation for Zotero 7](https://www.zotero.org/support/dev/zotero_7_for_developers) + +[๐Ÿ› ๏ธ Zotero Plugin Toolkit](https://github.com/windingwind/zotero-plugin-toolkit) | [API Documentation](https://github.com/windingwind/zotero-plugin-toolkit/blob/master/docs/zotero-plugin-toolkit.md) + +[โ„น๏ธ Zotero Type Definitions](https://github.com/windingwind/zotero-types) + +[๐Ÿ“œ Zotero Source Code](https://github.com/zotero/zotero) + +[๐Ÿ“Œ Zotero Plugin Template](https://github.com/windingwind/zotero-plugin-template) (This repo) + +> [!tip] +> ๐Ÿ‘ Watch this repo so that you can be notified whenever there are fixes & updates. + +## Plugins built with this template [![GitHub Repo stars](https://img.shields.io/github/stars/windingwind/zotero-better-notes?label=zotero-better-notes&style=flat-square)](https://github.com/windingwind/zotero-better-notes) [![GitHub Repo stars](https://img.shields.io/github/stars/windingwind/zotero-pdf-preview?label=zotero-pdf-preview&style=flat-square)](https://github.com/windingwind/zotero-pdf-preview) @@ -26,23 +41,12 @@ Some plugins created with this template: [![GitHub Repo stars](https://img.shields.io/github/stars/MuiseDestiny/zotero-gpt?label=zotero-gpt&style=flat-square)](https://github.com/MuiseDestiny/zotero-gpt) [![GitHub Repo stars](https://img.shields.io/github/stars/zoushucai/zotero-journalabbr?label=zotero-journalabbr&style=flat-square)](https://github.com/zoushucai/zotero-journalabbr) [![GitHub Repo stars](https://img.shields.io/github/stars/MuiseDestiny/zotero-figure?label=zotero-figure&style=flat-square)](https://github.com/MuiseDestiny/zotero-figure) -[![GitHub Repo stars](https://img.shields.io/github/stars/MuiseDestiny/zotero-file?label=WanderingFile&style=flat-square)](https://github.com/MuiseDestiny/zotero-file) [![GitHub Repo stars](https://img.shields.io/github/stars/l0o0/jasminum?label=jasminum&style=flat-square)](https://github.com/l0o0/jasminum) [![GitHub Repo stars](https://img.shields.io/github/stars/lifan0127/ai-research-assistant?label=ai-research-assistant&style=flat-square)](https://github.com/lifan0127/ai-research-assistant) -๐Ÿ“– [Plugin Development Documentation](https://zotero.yuque.com/books/share/8d230829-6004-4934-b4c6-685a7001bfa0/vec88d) (Chinese, provides English translation) +If you are using this repo, I recommended that you put the following badge on your README: -๐Ÿ› ๏ธ [Zotero Plugin Toolkit](https://github.com/windingwind/zotero-plugin-toolkit) | [API Documentation](https://github.com/windingwind/zotero-plugin-toolkit/blob/master/docs/zotero-plugin-toolkit.md) - -โ„น๏ธ [Zotero Type Definitions](https://github.com/windingwind/zotero-types) - -๐Ÿ“œ [Zotero Source Code](https://github.com/zotero/zotero) - -๐Ÿ“Œ [Zotero Plugin Template](https://github.com/windingwind/zotero-plugin-template) (This repo) - -> ๐Ÿ‘ Watch this repo so that you can be notified whenever there are fixes & updates. - -If you are using this repo, I recommended that you put this badge ([![Using Zotero Plugin Template](https://img.shields.io/badge/Using-Zotero%20Plugin%20Template-blue?style=flat-square&logo=github)](https://github.com/windingwind/zotero-plugin-template)) on your README: +[![Using Zotero Plugin Template](https://img.shields.io/badge/Using-Zotero%20Plugin%20Template-blue?style=flat-square&logo=github)](https://github.com/windingwind/zotero-plugin-template) ```md [![Using Zotero Plugin Template](https://img.shields.io/badge/Using-Zotero%20Plugin%20Template-blue?style=flat-square&logo=github)](https://github.com/windingwind/zotero-plugin-template) @@ -50,19 +54,21 @@ If you are using this repo, I recommended that you put this badge ([![Using Zote ## Features -> โ—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`. - - Event-driven, functional programming, under extensive skeleton; - Simple and user-friendly, works out-of-the-box. -- โญ[New!]Auto hot reload! Whenever the source code is modified, automatically compile and reload. [See hereโ†’](#auto-hot-reload) -- Abundant examples in `src/modules/examples.ts`, covering most of the commonly used APIs in plugins(using [zotero-plugin-toolkit](https://github.com/windingwind/zotero-plugin-toolkit)); +- โญ [New!] Auto hot reload! Whenever the source code is modified, automatically compile and reload. [See hereโ†’](#auto-hot-reload) +- Abundant examples in `src/modules/examples.ts`, covering most of the commonly used APIs in plugins (using [zotero-plugin-toolkit](https://github.com/windingwind/zotero-plugin-toolkit)); - TypeScript support: - - Full type definition support for the whole Zotero project, which is written in JavaScript(using [zotero-types](https://github.com/windingwind/zotero-types)); + - Full type definition support for the whole Zotero project, which is written in JavaScript (using [zotero-types](https://github.com/windingwind/zotero-types)); - Global variables and environment setup; -- Plugin build/test/release workflow: - - Automatically generate/update plugin id/version, update configrations, and set environment variables(`development/production`); +- 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 (using [release-it](https://github.com/release-it/release-it)); +- 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 @@ -129,80 +135,60 @@ Activate with `Shift+P`. ## Quick Start Guide -### Install Pre-built `xpi` +### 0 Requirement -See how the examples work by directly downloading the `xpi` file from GitHub release and install it to your Zotero. +1. Install a beta version of Zotero: +2. Install [Node.js](https://nodejs.org/en/) and Git -This is also how your plugin will be released and used by others. +> [!note] +> This guide assumes that you have an initial understanding of the basic structure and workings of the Zotero plugin. If you don't, please refer to the [documentation](https://www.zotero.org/support/dev/zotero_7_for_developers) and official plugin examples [Make It Red](https://github.com/zotero/make-it-red) first. -> The release do not promise any real functions. It is probably not up-to-date. -> -> The `xpi` package is a zip file. However, please don't modify it directly. Modify the source code and build it. +### 1 Creat Your Repo -### Build from Source +1. Click `Use this template` +2. Git clone your new repo +
+ ๐Ÿ’ก Start with GitHub Codespace -- Fork this repo/Click `Use this template`; -- Git clone the forked repo; -- Enter the repo folder; -
-๐Ÿ’ก Start with GitHub Codespace + _GitHub CodeSpace_ enables you getting started without the need to download code/IDE/dependencies locally. -_GitHub CodeSpace_ enables you getting started without the need to download code/IDE/dependencies locally. + Replace the steps above and build you first plugin in 30 seconds! -Replace the steps above and build you first plugin in 30 seconds! + - Goto top of the [homepage](https://github.com/windingwind/zotero-plugin-template), click the green button `Use this template`, click `Open in codespace`. You may need to login to your GitHub account. + - Wait for codespace to load. -- Goto top of the [homepage](https://github.com/windingwind/zotero-plugin-template), click the green button `Use this template`, click `Open in codespace`. You may need to login to your GitHub account. -- Wait for codespace to load. +
-
+3. Enter the repo folder -- Modify the settings in `./package.json`, including: - - ```json5 - { - version, - author, - description, - homepage, - config { - releasepage, // URL to releases(`.xpi`) - updaterdf, // URL to update.json - 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} - } - } - ``` - - > Be careful to set the addonID and addonRef to avoid conflict. - -- Run `npm install` to set up the plugin and install dependencies. If you don't have NodeJS installed, please download it [here](https://nodejs.org/en/); -- Run `npm run build` to build the plugin in production mode. Run `npm run build-dev` to build the plugin in development mode. The xpi for installation and the built code is under `build` folder. - - > What the difference between dev & prod? - > - > - This environment variable is stored in `Zotero.${addonInstance}.data.env`. The outputs to console is disabled in prod mode. - > - You can decide what users cannot see/use based on this variable. - -### Release +### 2 Config Template Settings and Enviroment -To build and release, use - -```shell -# A release-it command: version increase, npm run build, git push, and GitHub release -# You need to set the environment variable GITHUB_TOKEN https://github.com/settings/tokens -# release-it: https://github.com/release-it/release-it -npm run release -``` +1. Modify the settings in `./package.json`, including: -### Setup Development Environment + ```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 + }, + } + ``` -1. Install a beta version of Zotero: (Zotero 7 beta: ) + > [!warning] + > Be careful to set the addonID and addonRef to avoid conflict. -2. Install Firefox 102 (for Zotero 7) + 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. -3. Copy zotero command line config file. Modify the commands that starts your installation of the beta Zotero. +2. Copy zotero command line config 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. @@ -212,27 +198,26 @@ npm run release vim ./scripts/zotero-cmd.json ``` -4. Build plugin and restart Zotero with `npm run restart`. - -5. Launch Firefox 102 (Zotero 7) - -6. In Firefox, go to devtools, go to settings, click "enable remote debugging" and the one next to it that's also about debugging +3. Install dependencies with `npm install` - > Enter `about:debugging#/setup` in FF 102. + > If you are using `pnpm` as the package manager for your project, you need to add `public-hoist-pattern[]=*@types/bluebird*` to `.npmrc`, see . -7. In Zotero, go to setting, advanced, config editor, look up "debugging" and click on "allow remote debugging". +### 3 Coding -8. Connect to Zotero in Firefox. In FF 102, enter `localhost:6100` in the bottom input of remote-debugging page and click `add`. +Start development server with `npm start`, it will: -9. Click `connect` in the leftside-bar of Firefox remote-debugging page. +- 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 -10. Click "Inspect Main Process" - -### Auto Hot Reload +#### Auto Hot Reload Tired of endless restarting? Forget about it! -1. Run `npm run start-watch`. (If Zotero is already running, use `npm run watch`) +1. Run `npm start`. 2. Coding. (Yes, that's all) When file changes are detected in `src` or `addon`, the plugin will be automatically compiled and reloaded. @@ -240,27 +225,73 @@ When file changes are detected in `src` or `addon`, the plugin will be automatic
๐Ÿ’ก Steps to add this feature to an existing plugin -1. Copy `scripts/reload.mjs` -2. Copy `reload`, `watch`, and `start-watch` commands in `package.json` -3. Run `npm install --save-dev chokidar-cli` +1. Copy `scripts/**.mjs` +2. Copy `server`, `build`, and `stop` commands in `package.json` +3. Run `npm install --save-dev chokidar` 4. Done.
-### Debug in Zotero +#### Debug in Zotero You can also: -- Test code snipastes in Tools->Developer->Run Javascript; +- Test code snipastes in Tools -> Developer -> Run Javascript; - Debug output with `Zotero.debug()`. Find the outputs in Help->Debug Output Logging->View Output; - Debug UI. Zotero is built on the Firefox XUL framework. Debug XUL UI with software like [XUL Explorer](https://udn.realityripple.com/docs/Archive/Mozilla/XUL_Explorer). > XUL Documentation: +### 4 Build + +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`: + +- Create/empty `build/`. +- Copy `addon/**` to `build/addon/**` +- Replace placeholders: use `replace-in-file` to replace keywords and configurations defined in `package.json` in non-build files (`xhtml`, `json`, et al.). +- Prepare locale files to [avaid 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`. +- (Production mode only) Zip the `./build/addon` to `./build/*.xpi` +- (Production mode only) Prepare `update.json` or `update-beta.json` + +> [!note] +> +> **What's the difference between dev & prod?** +> +> - This environment variable is stored in `Zotero.${addonInstance}.data.env`. The outputs to console is disabled in prod mode. +> - You can decide what users cannot see/use based on this variable. +> - In production mode, the build script will pack the plugin and update the `update.json` + +### 5 Release + +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 +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 + +#### About Prerelease + +The template defines `prerelease` as the beta version of the plugin, when you select a `prerelease` version in release-it (with `-` in the version number), the build script will create a new `update-beta.json` for prerelease use, which ensures that users of the regular version won't be able to update to the beta, only users who have manually downloaded and installed the beta will be able to update to the next beta automatically. When the next regular release is updated, both `update.json` and `update-beta.json` will be updated so that both regular and beta users can update to the new regular release. + +> [!warning] +> Strictly, distinguishing between Zotero 6 and Zotero 7 compatible plugin versions should be done by configuring `applications.zotero.strict_min_version` in `addons.__addonID__.updates[]` of `update.json` respectively, so that Zotero recognizes it properly, see . + ## Details ### About Hooks -> See also [`src/hooks.ts`](https://github.com/windingwind/zotero-plugin-template/blob/bootstrap/src/hooks.ts) +> See also [`src/hooks.ts`](https://github.com/windingwind/zotero-plugin-template/blob/main/src/hooks.ts) 1. When install/enable/startup triggered from Zotero, `bootstrap.js` > `startup` is called - Wait for Zotero ready @@ -274,7 +305,7 @@ You can also: ### About Global Variables -> See also [`src/index.ts`](https://github.com/windingwind/zotero-plugin-template/blob/bootstrap/src/index.ts) +> See also [`src/index.ts`](https://github.com/windingwind/zotero-plugin-template/blob/main/src/index.ts) The bootstrapped plugin runs in a sandbox, which does not have default global variables like `Zotero` or `window`, which we used to have in the overlay plugins' window environment. @@ -297,20 +328,6 @@ createElement(document, "hbox"); // returns XUL.Box createElement(document, "button", { namespace: "xul" }); // manually set namespace. returns XUL.Button ``` -### About Build - -Use Esbuild to build `.ts` source code to `.js`. - -Use `replace-in-file` to replace keywords and configurations defined in `package.json` in non-build files (`xhtml`, `.flt`, et. al.). - -Steps in `scripts/build.mjs`: - -1. Clean `./build` -2. Copy `./addon` to `./build` -3. Esbuild to `./build/addon/chrome/content/scripts` -4. Replace `__buildVersion__` and `__buildTime__` in `./build/addon` -5. Zip the `./build/addon` to `./build/*.xpi` - ### About Zotero API Zotero docs are outdated and incomplete. Clone and search the keyword globally. @@ -367,11 +384,14 @@ This section shows the directory structure of a template. | `-- prefs.js |-- build/ # build dir |-- scripts # scripts for dev -| |-- build.mjs # esbuild and replace -| |-- reload.mjs -| |-- start.mjs -| |-- stop.mjs -| `-- zotero-cmd-default.json +| |-- 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-default.json # example of local env |-- src # source code | |-- addon.ts # base class | |-- hooks.ts # lifecycle hooks @@ -387,7 +407,6 @@ This section shows the directory structure of a template. |-- tsconfig.json # https://code.visualstudio.com/docs/languages/jsconfig |-- typings # ts typings | `-- global.d.ts -|-- update-template.json # template of `update.json` `-- update.json ``` diff --git a/package.json b/package.json index 1a26e16..86a2b4b 100644 --- a/package.json +++ b/package.json @@ -9,26 +9,16 @@ "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", - "updateBetaJSON": "https://raw.githubusercontent.com/windingwind/zotero-addon-template/main/update-beta.json" + "updateJSON": "https://raw.githubusercontent.com/windingwind/zotero-addon-template/main/update.json" }, "main": "src/index.ts", "scripts": { - "build-dev": "cross-env NODE_ENV=development node scripts/build.mjs", - "build-prod": "cross-env NODE_ENV=production node scripts/build.mjs", - "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", + "start": "node scripts/server.mjs", + "build": "tsc --noEmit && node scripts/build.mjs production", "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", "test": "echo \"Error: no test specified\" && exit 1", + "release": "release-it", "update-deps": "npm update --save" }, "repository": { @@ -48,10 +38,8 @@ "@types/node": "^20.10.4", "@typescript-eslint/eslint-plugin": "^6.13.2", "@typescript-eslint/parser": "^6.13.2", - "chokidar-cli": "^3.0.0", + "chokidar": "^3.5.3", "compressing": "^1.10.0", - "concurrently": "^8.2.2", - "cross-env": "^7.0.3", "esbuild": "^0.19.8", "eslint": "^8.55.0", "eslint-config-prettier": "^9.1.0", @@ -60,5 +48,88 @@ "replace-in-file": "^7.0.2", "typescript": "^5.3.3", "zotero-types": "^1.3.7" + }, + "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", + "after:release": "echo Successfully released ${name} v${version} to ${repo.repository}." + } } } diff --git a/scripts/build.mjs b/scripts/build.mjs index 8e61172..c1c8521 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -1,104 +1,68 @@ -import { build } from "esbuild"; +import details from "../package.json" assert { 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 { - existsSync, - lstatSync, - writeFileSync, - readFileSync, - mkdirSync, - readdirSync, - rmSync, - renameSync, -} from "fs"; import { env, exit } from "process"; import replaceInFile from "replace-in-file"; + 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 { name, author, description, homepage, version, config } = details; const isPreRelease = version.includes("-"); -// If it is a pre-release, use update-beta.json -config.updateURL = isPreRelease ? config.updateBetaJSON : config.updateJSON; - -const updateJSONFile = isPreRelease ? "update-beta.json" : "update.json"; -const updateLink = isPreRelease - ? `${config.releasePage}/download/v${version}/${name}.xpi` - : `${config.releasePage}/latest/download/${name}.xpi`; - -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)); -} - -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); - } +function replaceString(buildTime) { + const replaceFrom = [ + /__author__/g, + /__description__/g, + /__homepage__/g, + /__buildVersion__/g, + /__buildTime__/g, + ]; + const replaceTo = [author, description, homepage, version, buildTime]; - // 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); - } - }); - } -} + config.updateURL = isPreRelease + ? config.updateJSON.replace("update.json", "update-beta.json") + : config.updateJSON; -function clearFolder(target) { - if (existsSync(target)) { - rmSync(target, { recursive: true, force: true }); - } + replaceFrom.push( + ...Object.keys(config).map((k) => new RegExp(`__${k}__`, "g")), + ); + replaceTo.push(...Object.values(config)); - mkdirSync(target, { recursive: true }); -} + const replaceResult = replaceInFileSync({ + files: [ + `${buildDir}/addon/**/*.xhtml`, + `${buildDir}/addon/**/*.json`, + `${buildDir}/addon/prefs.js`, + `${buildDir}/addon/manifest.json`, + `${buildDir}/addon/bootstrap.js`, + ], + from: replaceFrom, + to: replaceTo, + countMatches: 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; + // Logger.debug( + // "[Build] Run replace in ", + // replaceResult.filter((f) => f.hasChanged).map((f) => `${f.file} : ${f.numReplacements} / ${f.numMatches}`), + // ); } -function renameLocaleFiles() { +function prepareLocaleFiles() { + // Walk the builds/addon/locale folder's sub folders and rename *.ftl to addonRef-*.ftl const localeDir = path.join(buildDir, "addon/locale"); const localeFolders = readdirSync(localeDir, { withFileTypes: true }) .filter((dirent) => dirent.isDirectory()) @@ -121,48 +85,6 @@ function renameLocaleFiles() { } } } -} - -function replaceString() { - const replaceFrom = [ - /__author__/g, - /__description__/g, - /__homepage__/g, - /__buildVersion__/g, - /__buildTime__/g, - /__updateLink__/g, - ]; - const replaceTo = [ - author, - description, - homepage, - version, - buildTime, - updateLink, - ]; - - replaceFrom.push( - ...Object.keys(config).map((k) => new RegExp(`__${k}__`, "g")), - ); - replaceTo.push(...Object.values(config)); - - const optionsAddon = { - files: [ - `${buildDir}/addon/**/*.xhtml`, - `${buildDir}/addon/**/*.html`, - `${buildDir}/addon/**/*.json`, - `${buildDir}/addon/prefs.js`, - `${buildDir}/addon/manifest.json`, - `${buildDir}/addon/bootstrap.js`, - ], - from: replaceFrom, - to: replaceTo, - countMatches: true, - }; - - optionsAddon.files.push(updateJSONFile); - - const replaceResult = replaceInFileSync(optionsAddon); const localeMessage = new Set(); const localeMessageMiss = new Set(); @@ -205,43 +127,83 @@ function replaceString() { }, }); - console.log( - "[Build] Run replace in ", - replaceResult - .filter((f) => f.hasChanged) - .map((f) => `${f.file} : ${f.numReplacements} / ${f.numMatches}`), - replaceResultFlt.filter((f) => f.hasChanged).map((f) => `${f.file} : OK`), - replaceResultXhtml.filter((f) => f.hasChanged).map((f) => `${f.file} : OK`), + Logger.debug( + "[Build] Prepare locale files OK", + // replaceResultFlt.filter((f) => f.hasChanged).map((f) => `${f.file} : OK`), + // replaceResultXhtml.filter((f) => f.hasChanged).map((f) => `${f.file} : OK`), ); if (localeMessageMiss.size !== 0) { - console.warn( - `[Build] [Warn] Fluent message [${new Array( + Logger.warn( + `[Build] Fluent message [${new Array( ...localeMessageMiss, )}] do not exsit in addon's locale files.`, ); } } -async function esbuild() { - await build({ - 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: true, - }).catch(() => exit(1)); +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}`), + ); } -async function main() { - console.log( +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, ]}`, @@ -250,37 +212,37 @@ async function main() { clearFolder(buildDir); copyFolderRecursiveSync("addon", buildDir); + replaceString(buildTime); + Logger.debug("[Build] Replace OK"); - copyFileSync("update-template.json", updateJSONFile); - - await esbuild(); + prepareLocaleFiles(); - console.log("[Build] Run esbuild OK"); + await build(esbuildOptions); + Logger.debug("[Build] Run esbuild OK"); - replaceString(); + Logger.debug("[Build] Addon prepare OK"); - console.log("[Build] Replace OK"); - - // Walk the builds/addon/locale folder's sub folders and rename *.ftl to addonRef-*.ftl - renameLocaleFiles(); + if (process.env.NODE_ENV === "production") { + await zip.compressDir( + path.join(buildDir, "addon"), + path.join(buildDir, `${name}.xpi`), + { + ignoreBase: true, + }, + ); + Logger.debug("[Build] Addon pack OK"); - console.log("[Build] Addon prepare OK"); + prepareUpdateJson(); - 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.`, - ); + Logger.debug( + `[Build] Finished in ${(new Date().getTime() - t.getTime()) / 1000} s.`, + ); + } } -main().catch((err) => { - console.log(err); - exit(1); -}); +if (process.env.NODE_ENV === "production") { + main().catch((err) => { + Logger.error(err); + exit(1); + }); +} diff --git a/scripts/reload.mjs b/scripts/reload.mjs deleted file mode 100644 index 5cf84c7..0000000 --- a/scripts/reload.mjs +++ /dev/null @@ -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); diff --git a/scripts/scripts.mjs b/scripts/scripts.mjs new file mode 100644 index 0000000..126d8e6 --- /dev/null +++ b/scripts/scripts.mjs @@ -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); +} +})()`; diff --git a/scripts/server.mjs b/scripts/server.mjs new file mode 100644 index 0000000..8a94805 --- /dev/null +++ b/scripts/server.mjs @@ -0,0 +1,90 @@ +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(); + setTimeout(() => { + openDevTool(); + }, 2000); + + // 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); +}); diff --git a/scripts/start.mjs b/scripts/start.mjs index 118b5a0..2b9782b 100644 --- a/scripts/start.mjs +++ b/scripts/start.mjs @@ -1,33 +1,36 @@ -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 { Logger } from "./utils.mjs"; import cmd from "./zotero-cmd.json" assert { type: "json" }; +import { spawn } from "child_process"; +import { existsSync, readFileSync, writeFileSync } 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; +const logPath = "logs"; +const logFilePath = path.join(logPath, "zotero.log"); + if (!existsSync(zoteroBinPath)) { 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 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() { writeFileSync(addonProxyFilePath, buildPath); - console.log( - `[info] Addon proxy file has been updated. - File path: ${addonProxyFilePath} - Addon path: ${buildPath} `, + Logger.debug( + `Addon proxy file has been updated. + File path: ${addonProxyFilePath} + Addon path: ${buildPath} `, ); } @@ -36,12 +39,6 @@ if (existsSync(profilePath)) { writeAddonProxyFile(); } } else { - if ( - existsSync(profilePath) && - !existsSync(path.join(profilePath, "extensions")) - ) { - mkdirSync(path.join(profilePath, "extensions")); - } writeAddonProxyFile(); } @@ -62,13 +59,47 @@ if (existsSync(profilePath)) { }); const updatedPrefs = filteredLines.join("\n"); writeFileSync(prefsPath, updatedPrefs, "utf-8"); - console.log("[info] The /prefs.js has been modified."); + Logger.debug("The /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, ""); +} + +export function main() { + prepareDevEnv(); -execSync(startZotero); -exit(0); + prepareLog(); + + const zoteroProcess = spawn(zoteroBinPath, [ + "--debugger", + "--purgecaches", + "-profile", + profilePath, + ]); + + zoteroProcess.stdout.on("data", (data) => { + 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(); + }); +} diff --git a/scripts/stop.mjs b/scripts/stop.mjs index 197ef04..3b44ef2 100644 --- a/scripts/stop.mjs +++ b/scripts/stop.mjs @@ -1,14 +1,26 @@ -import process from "process"; -import { execSync } from "child_process"; +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; -try { - if (process.platform === "win32") { - execSync(killZoteroWindows); +isRunning("zotero", (status) => { + if (status) { + killZotero(); } 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); } diff --git a/update-template.json b/scripts/update-template.json similarity index 100% rename from update-template.json rename to scripts/update-template.json diff --git a/scripts/utils.mjs b/scripts/utils.mjs new file mode 100644 index 0000000..d17288a --- /dev/null +++ b/scripts/utils.mjs @@ -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); + }); +} diff --git a/src/index.ts b/src/index.ts index cbc1423..f413754 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,14 +1,10 @@ import { BasicTool } from "zotero-plugin-toolkit/dist/basic"; import Addon from "./addon"; import { config } from "../package.json"; -import { waitUntil } from "./utils/wait"; const basicTool = new BasicTool(); if (!basicTool.getGlobal("Zotero")[config.addonInstance]) { - if (__env__ === "development") { - openDevTool(); - } defineGlobal("window"); defineGlobal("document"); defineGlobal("ZoteroPane"); @@ -29,46 +25,3 @@ function defineGlobal(name: string, getter?: () => any) { }, }); } - -function openDevTool() { - // 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); - - const env = - Services.env || - // @ts-ignore - mozIEnvironment is not in the types - 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", - { - // @ts-ignore - onLoad is not in the types - onLoad: (doc: Document) => { - ( - doc.querySelector("#status-message-container") as HTMLDivElement - ).style.visibility = "collapse"; - let toolboxBody: HTMLIFrameElement; - waitUntil( - () => { - toolboxBody = doc - .querySelector(".devtools-toolbox-browsertoolbox-iframe") - // @ts-ignore - contentDocument is not in the types - ?.contentDocument?.querySelector(".theme-body"); - return !!toolboxBody; - }, - () => { - toolboxBody.setAttribute("style", "pointer-events: all !important"); - }, - ); - }, - }, - ); -} diff --git a/update-beta.json b/update-beta.json deleted file mode 100644 index 146d3d7..0000000 --- a/update-beta.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "addons": { - "addontemplate@euclpts.com": { - "updates": [ - { - "version": "1.0.0", - "update_link": "https://github.com/windingwind/zotero-addon-template/releases/latest/download/zotero-addon-template.xpi", - "applications": { - "zotero": { - "strict_min_version": "6.999" - } - } - } - ] - } - } -}