From ff0aa2beb49d3143fb8cb25d1a0815f4da148ce5 Mon Sep 17 00:00:00 2001 From: windingwind Date: Mon, 12 Jun 2023 23:24:35 +0800 Subject: [PATCH] update: plugin template hot reload --- .vscode/launch.json | 79 +++++++------ addon/bootstrap.js | 3 + package.json | 16 ++- scripts/build.js | 204 -------------------------------- scripts/build.mjs | 192 ++++++++++++++++++++++++++++++ scripts/reload.mjs | 42 +++++++ scripts/{start.js => start.mjs} | 11 +- scripts/stop.js | 32 ----- scripts/stop.mjs | 14 +++ 9 files changed, 310 insertions(+), 283 deletions(-) delete mode 100644 scripts/build.js create mode 100644 scripts/build.mjs create mode 100644 scripts/reload.mjs rename scripts/{start.js => start.mjs} (65%) delete mode 100644 scripts/stop.js create mode 100644 scripts/stop.mjs diff --git a/.vscode/launch.json b/.vscode/launch.json index 9be09e3..da98665 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,38 +1,43 @@ { - // 使用 IntelliSense 了解相关属性。 - // 悬停以查看现有属性的描述。 - // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "Restart-Z6", - "runtimeExecutable": "npm", - "runtimeArgs": [ - "run", - "restart-dev-z6" - ] - }, - { - "type": "node", - "request": "launch", - "name": "Restart-Z7", - "runtimeExecutable": "npm", - "runtimeArgs": [ - "run", - "restart-dev-z7" - ] - }, - { - "type": "node", - "request": "launch", - "name": "Restart in Prod Mode", - "runtimeExecutable": "npm", - "runtimeArgs": [ - "run", - "restart-prod" - ] - } - ] -} \ No newline at end of file + // 使用 IntelliSense 了解相关属性。 + // 悬停以查看现有属性的描述。 + // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "StartDev", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "start-watch"] + }, + { + "type": "node", + "request": "launch", + "name": "Restart-Z6", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "restart-dev-z6"] + }, + { + "type": "node", + "request": "launch", + "name": "Restart-Z7", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "restart-dev-z7"] + }, + { + "type": "node", + "request": "launch", + "name": "Reload", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "reload"] + }, + { + "type": "node", + "request": "launch", + "name": "Restart in Prod Mode", + "runtimeExecutable": "npm", + "runtimeArgs": ["run", "restart-prod"] + } + ] +} diff --git a/addon/bootstrap.js b/addon/bootstrap.js index e11e17a..f227d4d 100644 --- a/addon/bootstrap.js +++ b/addon/bootstrap.js @@ -108,6 +108,9 @@ function shutdown({ id, version, resourceURI, rootURI }, reason) { if (reason === APP_SHUTDOWN) { return; } + if (reason == ADDON_DISABLE) { + Services.obs.notifyObservers(null, "startupcache-invalidate", null); + } if (typeof Zotero === "undefined") { Zotero = Components.classes["@zotero.org/Zotero;1"].getService( Components.interfaces.nsISupports diff --git a/package.json b/package.json index 7d6e76a..f3732a9 100644 --- a/package.json +++ b/package.json @@ -13,19 +13,22 @@ }, "main": "src/index.ts", "scripts": { - "build-dev": "cross-env NODE_ENV=development node scripts/build.js", - "build-prod": "cross-env NODE_ENV=production node scripts/build.js", + "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-z6": "node scripts/start.js --z 6", - "start-z7": "node scripts/start.js --z 7", - "start": "node scripts/start.js", - "stop": "node scripts/stop.js", + "start-z6": "node scripts/start.mjs --z 6", + "start-z7": "node scripts/start.mjs --z 7", + "start": "node scripts/start.mjs", + "stop": "node scripts/stop.mjs", + "start-watch": "concurrently -c auto npm:start npm:watch", "restart-dev-z6": "npm run build-dev && npm run stop && npm run start-z6", "restart-dev-z7": "npm run build-dev && npm run stop && npm run start-z7", "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 --z 7", + "watch": "chokidar \"src/*.*\" \"addon/*.*\" -c \"npm run reload\"", "release": "release-it", "test": "echo \"Error: no test specified\" && exit 1", "update-deps": "npm update --save" @@ -73,6 +76,7 @@ "@types/node": "^20.3.0", "@types/seedrandom": "^3.0.5", "@types/yamljs": "^0.2.31", + "chokidar-cli": "^3.0.0", "compressing": "^1.9.0", "concurrently": "^7.6.0", "cross-env": "^7.0.3", diff --git a/scripts/build.js b/scripts/build.js deleted file mode 100644 index 42c2f5f..0000000 --- a/scripts/build.js +++ /dev/null @@ -1,204 +0,0 @@ -const esbuild = require("esbuild"); -const compressing = require("compressing"); -const path = require("path"); -const fs = require("fs"); -const process = require("process"); -const replace = require("replace-in-file"); -const { - name, - author, - description, - homepage, - version, - config, -} = require("../package.json"); - -function copyFileSync(source, target) { - var targetFile = target; - - // If target is a directory, a new file with the same name will be created - if (fs.existsSync(target)) { - if (fs.lstatSync(target).isDirectory()) { - targetFile = path.join(target, path.basename(source)); - } - } - - fs.writeFileSync(targetFile, fs.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 (!fs.existsSync(targetFolder)) { - fs.mkdirSync(targetFolder); - } - - // Copy - if (fs.lstatSync(source).isDirectory()) { - files = fs.readdirSync(source); - files.forEach(function (file) { - var curSource = path.join(source, file); - if (fs.lstatSync(curSource).isDirectory()) { - copyFolderRecursiveSync(curSource, targetFolder); - } else { - copyFileSync(curSource, targetFolder); - } - }); - } -} - -function clearFolder(target) { - if (fs.existsSync(target)) { - fs.rmSync(target, { recursive: true, force: true }); - } - - fs.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; -} - -async function main() { - const t = new Date(); - const buildTime = dateFormat("YYYY-mm-dd HH:MM:SS", t); - const buildDir = "builds"; - - console.log( - `[Build] BUILD_DIR=${buildDir}, VERSION=${version}, BUILD_TIME=${buildTime}, ENV=${[ - process.env.NODE_ENV, - ]}` - ); - - clearFolder(buildDir); - - copyFolderRecursiveSync("addon", buildDir); - - copyFileSync("update-template.json", "update.json"); - copyFileSync("update-template.rdf", "update.rdf"); - - await esbuild - .build({ - entryPoints: ["src/index.ts"], - define: { - __env__: `"${process.env.NODE_ENV}"`, - }, - bundle: true, - outfile: path.join(buildDir, "addon/chrome/content/scripts/index.js"), - // Don't turn minify on - // minify: true, - target: ["firefox60"], - }) - .catch(() => process.exit(1)); - - await esbuild - .build({ - entryPoints: ["src/extras/editorScript.ts"], - bundle: true, - outfile: path.join( - buildDir, - "addon/chrome/content/scripts/editorScript.js" - ), - target: ["firefox60"], - }) - .catch(() => process.exit(1)); - - await esbuild - .build({ - entryPoints: ["src/extras/docxWorker.ts"], - bundle: true, - outfile: path.join( - buildDir, - "addon/chrome/content/scripts/docxWorker.js" - ), - target: ["firefox60"], - }) - .catch(() => process.exit(1)); - - console.log("[Build] Run esbuild OK"); - - 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: [ - path.join(buildDir, "**/*.rdf"), - path.join(buildDir, "**/*.dtd"), - path.join(buildDir, "**/*.xul"), - path.join(buildDir, "**/*.html"), - path.join(buildDir, "**/*.xhtml"), - path.join(buildDir, "**/*.json"), - path.join(buildDir, "addon/prefs.js"), - path.join(buildDir, "addon/chrome.manifest"), - path.join(buildDir, "addon/manifest.json"), - path.join(buildDir, "addon/bootstrap.js"), - "update.json", - "update.rdf", - ], - from: replaceFrom, - to: replaceTo, - countMatches: true, - }; - - _ = replace.sync(optionsAddon); - console.log( - "[Build] Run replace in ", - _.filter((f) => f.hasChanged).map( - (f) => `${f.file} : ${f.numReplacements} / ${f.numMatches}` - ) - ); - - console.log("[Build] Replace OK"); - - console.log("[Build] Addon prepare OK"); - - compressing.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); - process.exit(1); -}); diff --git a/scripts/build.mjs b/scripts/build.mjs new file mode 100644 index 0000000..5ff8cc6 --- /dev/null +++ b/scripts/build.mjs @@ -0,0 +1,192 @@ +import { build } from "esbuild"; +import { zip } from "compressing"; +import { join, basename } from "path"; +import { + existsSync, + lstatSync, + writeFileSync, + readFileSync, + mkdirSync, + readdirSync, + rmSync, +} from "fs"; +import { env, exit } from "process"; +import replaceInFile from "replace-in-file"; +const { sync: replaceSync } = replaceInFile; +import details from "../package.json" assert { type: "json" }; + +const { name, author, description, homepage, version, config } = details; + +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 = join(target, basename(source)); + } + } + + writeFileSync(targetFile, readFileSync(source)); +} + +function copyFolderRecursiveSync(source, target) { + var files = []; + + // Check if folder needs to be created or integrated + var targetFolder = join(target, basename(source)); + if (!existsSync(targetFolder)) { + mkdirSync(targetFolder); + } + + // Copy + if (lstatSync(source).isDirectory()) { + files = readdirSync(source); + files.forEach(function (file) { + var curSource = 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; +} + +async function main() { + const t = new Date(); + const buildTime = dateFormat("YYYY-mm-dd HH:MM:SS", t); + const buildDir = "builds"; + + console.log( + `[Build] BUILD_DIR=${buildDir}, VERSION=${version}, BUILD_TIME=${buildTime}, ENV=${[ + env.NODE_ENV, + ]}` + ); + + clearFolder(buildDir); + + copyFolderRecursiveSync("addon", buildDir); + + copyFileSync("update-template.json", "update.json"); + copyFileSync("update-template.rdf", "update.rdf"); + + await build({ + entryPoints: ["src/index.ts"], + define: { + __env__: `"${env.NODE_ENV}"`, + }, + bundle: true, + outfile: join(buildDir, "addon/chrome/content/scripts/index.js"), + // Don't turn minify on + // minify: true, + target: ["firefox60"], + }).catch(() => exit(1)); + + await build({ + entryPoints: ["src/extras/editorScript.ts"], + bundle: true, + outfile: join(buildDir, "addon/chrome/content/scripts/editorScript.js"), + target: ["firefox60"], + }).catch(() => exit(1)); + + await build({ + entryPoints: ["src/extras/docxWorker.ts"], + bundle: true, + outfile: join(buildDir, "addon/chrome/content/scripts/docxWorker.js"), + target: ["firefox60"], + }).catch(() => exit(1)); + + console.log("[Build] Run esbuild OK"); + + 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: [ + join(buildDir, "**/*.rdf"), + join(buildDir, "**/*.dtd"), + join(buildDir, "**/*.xul"), + join(buildDir, "**/*.html"), + join(buildDir, "**/*.xhtml"), + join(buildDir, "**/*.json"), + join(buildDir, "addon/prejs"), + join(buildDir, "addon/chrome.manifest"), + join(buildDir, "addon/manifest.json"), + join(buildDir, "addon/bootstrap.js"), + "update.json", + "update.rdf", + ], + from: replaceFrom, + to: replaceTo, + countMatches: true, + }; + + const replaceResult = replaceSync(optionsAddon); + console.log( + "[Build] Run replace in ", + replaceResult + .filter((f) => f.hasChanged) + .map((f) => `${f.file} : ${f.numReplacements} / ${f.numMatches}`) + ); + + console.log("[Build] Replace OK"); + + console.log("[Build] Addon prepare OK"); + + zip.compressDir(join(buildDir, "addon"), 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); +}); diff --git a/scripts/reload.mjs b/scripts/reload.mjs new file mode 100644 index 0000000..34e9cbd --- /dev/null +++ b/scripts/reload.mjs @@ -0,0 +1,42 @@ +import { exit, argv } from "process"; +import minimist from "minimist"; +import { execSync } from "child_process"; +import details from "../package.json" assert { type: "json" }; +const { addonID, addonName } = details.config; +const version = details.version; +import cmd from "./zotero-cmd.json" assert { type: "json" }; +const { exec } = cmd; + +// Run node reload.js -h for help +const args = minimist(argv.slice(2)); + +const zoteroPath = exec[args.zotero || args.z || Object.keys(exec)[0]]; +const profile = args.profile || args.p; +const startZotero = `${zoteroPath} --debugger --purgecaches ${ + profile ? `-p ${profile}` : "" +}`; + +const script = ` +(async () => { + const { AddonManager } = ChromeUtils.import("resource://gre/modules/AddonManager.jsm"); + const addon = await AddonManager.getAddonByID("${addonID}"); + addon.disable(); + await Zotero.Promise.delay(1000); + addon.enable(); + 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/start.js b/scripts/start.mjs similarity index 65% rename from scripts/start.js rename to scripts/start.mjs index 2485412..5cec962 100644 --- a/scripts/start.js +++ b/scripts/start.mjs @@ -1,9 +1,12 @@ -const { execSync } = require("child_process"); -const { exit } = require("process"); -const { exec } = require("./zotero-cmd.json"); +import process from "process"; +import { execSync } from "child_process"; +import { exit } from "process"; +import minimist from "minimist"; +import cmd from "./zotero-cmd.json" assert { type: "json" }; +const { exec } = cmd; // Run node start.js -h for help -const args = require("minimist")(process.argv.slice(2)); +const args = minimist(process.argv.slice(2)); if (args.help || args.h) { console.log("Start Zotero Args:"); diff --git a/scripts/stop.js b/scripts/stop.js deleted file mode 100644 index b8fa030..0000000 --- a/scripts/stop.js +++ /dev/null @@ -1,32 +0,0 @@ -const { execSync } = require("child_process"); -const { killZoteroWindows, killZoteroUnix } = require("./zotero-cmd.json"); - -const MAX_WAIT_TIME = 10000; - -const startTime = new Date().getTime(); - -try { - if (process.platform === "win32") { - execSync(killZoteroWindows); - - // wait until zotero.exe is fully stopped. maximum wait for 10 seconds - while (new Date().getTime() - startTime <= MAX_WAIT_TIME) { - try { - execSync('tasklist | find /i "zotero.exe"'); - } catch (e) { - break; - } - } - } else { - execSync(killZoteroUnix); - - // wait until zotero is fully stopped. maximum wait for 10 seconds - while (new Date().getTime() - startTime <= MAX_WAIT_TIME) { - try { - execSync("ps aux | grep -i zotero"); - } catch (e) { - break; - } - } - } -} catch (e) {} diff --git a/scripts/stop.mjs b/scripts/stop.mjs new file mode 100644 index 0000000..197ef04 --- /dev/null +++ b/scripts/stop.mjs @@ -0,0 +1,14 @@ +import process from "process"; +import { execSync } from "child_process"; +import cmd from "./zotero-cmd.json" assert { type: "json" }; +const { killZoteroWindows, killZoteroUnix } = cmd; + +try { + if (process.platform === "win32") { + execSync(killZoteroWindows); + } else { + execSync(killZoteroUnix); + } +} catch (e) { + console.error(e); +}