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..4c3136c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ -**/build +build +logs node_modules package-lock.json -zotero-cmd.json \ No newline at end of file +pnpm-lock.yaml +yarn.lock +zotero-cmd.json +.DS_Store \ 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 083b14b..0000000 --- a/.release-it.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "npm": { - "publish": false - }, - "github": { - "release": true, - "assets": ["build/*.xpi"] - }, - "hooks": { - "after:bump": "npm run build", - "after:release": "echo Successfully released ${name} v${version} to ${repo.repository}." - } -} diff --git a/addon/manifest.json b/addon/manifest.json index d4b4ad7..798f1e2 100644 --- a/addon/manifest.json +++ b/addon/manifest.json @@ -12,7 +12,7 @@ "applications": { "zotero": { "id": "__addonID__", - "update_url": "__updaterdf__", + "update_url": "__updateURL__", "strict_min_version": "6.999", "strict_max_version": "7.0.*" } diff --git a/package.json b/package.json index 982acc1..1739082 100644 --- a/package.json +++ b/package.json @@ -10,25 +10,16 @@ "addonInstance": "BetterNotes", "dataSchemaVersion": "8", "releasepage": "https://github.com/windingwind/zotero-better-notes/releases/latest/download/zotero-better-notes.xpi", - "updaterdf": "https://raw.githubusercontent.com/windingwind/zotero-better-notes/master/update.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": "node scripts/server.mjs", + "build": "tsc --noEmit && node scripts/build.mjs production", "stop": "node scripts/stop.mjs", - "start-watch": "npm run build-dev && concurrently -c auto npm:start npm:watch", - "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 . '!addon/chrome/content/lib/**' && eslint . --ext .ts --fix", + "lint": "prettier --write . && eslint . --ext .ts --fix", "test": "echo \"Error: no test specified\" && exit 1", + "release": "release-it", "update-deps": "npm update --save" }, "repository": { @@ -100,5 +91,88 @@ "typescript": "^5.2.2", "xslt3": "^2.6.0", "zotero-types": "^1.3.5" + }, + "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-extras.mjs b/scripts/build-extras.mjs new file mode 100644 index 0000000..cefdd93 --- /dev/null +++ b/scripts/build-extras.mjs @@ -0,0 +1,23 @@ +import { build } from "esbuild"; +import path from "path"; + +const buildDir = "build"; + +export async function main() { + await build({ + entryPoints: ["src/extras/editorScript.ts"], + bundle: true, + outfile: path.join( + buildDir, + "addon/chrome/content/scripts/editorScript.js", + ), + target: ["firefox102"], + }).catch(() => exit(1)); + + await build({ + entryPoints: ["src/extras/docxWorker.ts"], + bundle: true, + outfile: path.join(buildDir, "addon/chrome/content/scripts/docxWorker.js"), + target: ["firefox102"], + }).catch(() => exit(1)); +} diff --git a/scripts/build.mjs b/scripts/build.mjs index 5dd49da..dd7ef23 100644 --- a/scripts/build.mjs +++ b/scripts/build.mjs @@ -1,96 +1,70 @@ -import { build } from "esbuild"; -import { zip } from "compressing"; -import path from "path"; +import details from "../package.json" assert { type: "json" }; import { - existsSync, - lstatSync, - writeFileSync, - readFileSync, - mkdirSync, - readdirSync, - rmSync, - renameSync, -} from "fs"; + 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"; + +import { main as buildExtras } from "./build-extras.mjs"; + 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("-"); -function copyFileSync(source, target) { - var targetFile = target; +function replaceString(buildTime) { + const replaceFrom = [ + /__author__/g, + /__description__/g, + /__homepage__/g, + /__buildVersion__/g, + /__buildTime__/g, + ]; + const replaceTo = [author, description, homepage, version, buildTime]; - // 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)); - } - } + config.updateURL = isPreRelease + ? config.updateJSON.replace("update.json", "update-beta.json") + : config.updateJSON; - writeFileSync(targetFile, readFileSync(source)); + 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/**/*.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 copyFolderRecursiveSync(source, target) { - var files = []; - - // Check if folder needs to be created or integrated - var targetFolder = path.join(target, path.basename(source)); - if (!existsSync(targetFolder)) { - mkdirSync(targetFolder); - } - - // Copy - if (lstatSync(source).isDirectory()) { - files = readdirSync(source); - files.forEach(function (file) { - var curSource = path.join(source, file); - if (lstatSync(curSource).isDirectory()) { - copyFolderRecursiveSync(curSource, targetFolder); - } else { - copyFileSync(curSource, targetFolder); - } - }); - } -} - -function clearFolder(target) { - if (existsSync(target)) { - rmSync(target, { recursive: true, force: true }); - } - - mkdirSync(target, { recursive: true }); -} - -function dateFormat(fmt, date) { - let ret; - const opt = { - "Y+": date.getFullYear().toString(), - "m+": (date.getMonth() + 1).toString(), - "d+": date.getDate().toString(), - "H+": date.getHours().toString(), - "M+": date.getMinutes().toString(), - "S+": date.getSeconds().toString(), - }; - for (let k in opt) { - ret = new RegExp("(" + k + ")").exec(fmt); - if (ret) { - fmt = fmt.replace( - ret[1], - ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, "0"), - ); - } - } - return fmt; -} - -function renameLocaleFiles() { +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()) @@ -113,42 +87,6 @@ function renameLocaleFiles() { } } } -} - -function replaceString() { - const replaceFrom = [ - /__author__/g, - /__description__/g, - /__homepage__/g, - /__buildVersion__/g, - /__buildTime__/g, - ]; - const replaceTo = [author, description, homepage, version, buildTime]; - - 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, - }; - - if (!isPreRelease) { - optionsAddon.files.push("update.json"); - } - - const replaceResult = replaceInFileSync(optionsAddon); const localeMessage = new Set(); const localeMessageMiss = new Set(); @@ -191,105 +129,126 @@ 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, - outfile: path.join( - buildDir, - `addon/chrome/content/scripts/${config.addonRef}.js`, - ), - // Don't turn minify on - // minify: true, - target: ["firefox102"], - }).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"); + } - await build({ - entryPoints: ["src/extras/editorScript.ts"], - bundle: true, - outfile: path.join( - buildDir, - "addon/chrome/content/scripts/editorScript.js", - ), - target: ["firefox102"], - }).catch(() => exit(1)); + const updateLink = + config.updateLink ?? isPreRelease + ? `${config.releasePage}/download/v${version}/${name}.xpi` + : `${config.releasePage}/latest/download/${name}.xpi`; - await build({ - entryPoints: ["src/extras/docxWorker.ts"], - bundle: true, - outfile: path.join(buildDir, "addon/chrome/content/scripts/docxWorker.js"), - target: ["firefox102"], - }).catch(() => exit(1)); + const replaceResult = replaceInFileSync({ + files: [ + "update-beta.json", + isPreRelease ? "pass" : "update.json", + `${buildDir}/addon/manifest.json`, + ], + from: [ + /__addonID__/g, + /__buildVersion__/g, + /__updateLink__/g, + /__updateURL__/g, + ], + to: [config.addonID, version, updateLink, config.updateURL], + countMatches: true, + }); + + Logger.debug( + `[Build] Prepare Update.json for ${ + isPreRelease + ? "\u001b[31m Prerelease \u001b[0m" + : "\u001b[32m Release \u001b[0m" + }`, + replaceResult + .filter((f) => f.hasChanged) + .map((f) => `${f.file} : ${f.numReplacements} / ${f.numMatches}`), + ); } -async function main() { - 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, - ]}, IS_PRE_RELEASE=${[env.IS_PRE_RELEASE]}`, + ]}`, ); clearFolder(buildDir); copyFolderRecursiveSync("addon", buildDir); + replaceString(buildTime); + Logger.debug("[Build] Replace OK"); - if (isPreRelease) { - console.log( - "[Build] [Warn] Running in pre-release mode. update.json will not be replaced.", + prepareLocaleFiles(); + + await build(esbuildOptions); + Logger.debug("[Build] Run esbuild OK"); + + Logger.debug("[Build] Addon prepare OK"); + + 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"); + + prepareUpdateJson(); + + Logger.debug( + `[Build] Finished in ${(new Date().getTime() - t.getTime()) / 1000} s.`, ); - } else { - copyFileSync("update-template.json", "update.json"); } - - await esbuild(); - - console.log("[Build] Run esbuild OK"); - - replaceString(); - - console.log("[Build] Replace OK"); - - // Walk the builds/addon/locale folder's sub folders and rename *.ftl to addonRef-*.ftl - renameLocaleFiles(); - - console.log("[Build] Addon prepare OK"); - - await zip.compressDir( - path.join(buildDir, "addon"), - path.join(buildDir, `${name}.xpi`), - { - ignoreBase: true, - }, - ); - - console.log("[Build] Addon pack OK"); - console.log( - `[Build] Finished in ${(new Date().getTime() - t.getTime()) / 1000} s.`, - ); } -main().catch((err) => { - console.log(err); - exit(1); -}); +if (process.env.NODE_ENV === "production") { + main().catch((err) => { + Logger.error(err); + exit(1); + }); + buildExtras().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..d99f4c7 --- /dev/null +++ b/scripts/server.mjs @@ -0,0 +1,90 @@ +import { main as build, esbuildOptions } from "./build.mjs"; +import { main as buildExtras } from "./build-extras.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(); + await buildExtras(); + } 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(); + await buildExtras(); + + // 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); +}); diff --git a/scripts/start.mjs b/scripts/start.mjs index 118b5a0..b916b5a 100644 --- a/scripts/start.mjs +++ b/scripts/start.mjs @@ -1,33 +1,39 @@ -import { execSync } from "child_process"; -import { exit } from "process"; -import { existsSync, writeFileSync, readFileSync, mkdirSync } from "fs"; -import path from "path"; import details from "../package.json" assert { type: "json" }; +import { 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; +// 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)) { +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 +42,6 @@ if (existsSync(profilePath)) { writeAddonProxyFile(); } } else { - if ( - existsSync(profilePath) && - !existsSync(path.join(profilePath, "extensions")) - ) { - mkdirSync(path.join(profilePath, "extensions")); - } writeAddonProxyFile(); } @@ -62,13 +62,53 @@ 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, ""); +} -execSync(startZotero); -exit(0); +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(); + }); +} 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/scripts/update-template.json b/scripts/update-template.json new file mode 100644 index 0000000..8f65a4c --- /dev/null +++ b/scripts/update-template.json @@ -0,0 +1,17 @@ +{ + "addons": { + "__addonID__": { + "updates": [ + { + "version": "__buildVersion__", + "update_link": "__updateLink__", + "applications": { + "zotero": { + "strict_min_version": "6.999" + } + } + } + ] + } + } +} 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/scripts/zotero-cmd-default.json b/scripts/zotero-cmd-template.json similarity index 100% rename from scripts/zotero-cmd-default.json rename to scripts/zotero-cmd-template.json diff --git a/src/index.ts b/src/index.ts index acb56ed..5f6a749 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,12 +6,10 @@ const basicTool = new BasicTool(); if (!basicTool.getGlobal("Zotero")[config.addonInstance]) { // Set global variables - _globalThis.Zotero = basicTool.getGlobal("Zotero"); defineGlobal("window"); defineGlobal("document"); defineGlobal("ZoteroPane"); defineGlobal("Zotero_Tabs"); - defineGlobal("OS"); _globalThis.addon = new Addon(); Object.defineProperty(_globalThis, "ztoolkit", { get() { diff --git a/update-beta.json b/update-beta.json new file mode 100644 index 0000000..be6e026 --- /dev/null +++ b/update-beta.json @@ -0,0 +1,17 @@ +{ + "addons": { + "Knowledge4Zotero@windingwind.com": { + "updates": [ + { + "version": "1.1.4-46", + "update_link": "undefined/download/v1.1.4-46/zotero-better-notes.xpi", + "applications": { + "zotero": { + "strict_min_version": "6.999" + } + } + } + ] + } + } +} diff --git a/update-template.json b/update-template.json deleted file mode 100644 index 7dca98e..0000000 --- a/update-template.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "addons": { - "__addonID__": { - "updates": [ - { - "version": "__buildVersion__", - "update_link": "__releasepage__", - "applications": { - "gecko": { - "strict_min_version": "60.0" - } - } - }, - { - "version": "__buildVersion__", - "update_link": "__releasepage__", - "applications": { - "zotero": { - "strict_min_version": "6.999" - } - } - } - ] - } - } -} diff --git a/update-template.rdf b/update-template.rdf deleted file mode 100644 index 1d453ca..0000000 --- a/update-template.rdf +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - 1.0.4 - - - zotero@chnm.gmu.edu - 5.999 - * - https://github.com/windingwind/zotero-better-notes/releases/download/1.0.4/zotero-better-notes.xpi - - - - - juris-m@juris-m.github.io - 5.999 - * - https://github.com/windingwind/zotero-better-notes/releases/download/1.0.4/zotero-better-notes.xpi - - - - - - - - \ No newline at end of file