update: plugin template hot reload

This commit is contained in:
windingwind 2023-06-12 23:24:35 +08:00
parent c56e94bc4d
commit ff0aa2beb4
9 changed files with 310 additions and 283 deletions

79
.vscode/launch.json vendored
View File

@ -1,38 +1,43 @@
{ {
// 使 IntelliSense // 使 IntelliSense
// //
// 访: https://go.microsoft.com/fwlink/?linkid=830387 // 访: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"type": "node", "type": "node",
"request": "launch", "request": "launch",
"name": "Restart-Z6", "name": "StartDev",
"runtimeExecutable": "npm", "runtimeExecutable": "npm",
"runtimeArgs": [ "runtimeArgs": ["run", "start-watch"]
"run", },
"restart-dev-z6" {
] "type": "node",
}, "request": "launch",
{ "name": "Restart-Z6",
"type": "node", "runtimeExecutable": "npm",
"request": "launch", "runtimeArgs": ["run", "restart-dev-z6"]
"name": "Restart-Z7", },
"runtimeExecutable": "npm", {
"runtimeArgs": [ "type": "node",
"run", "request": "launch",
"restart-dev-z7" "name": "Restart-Z7",
] "runtimeExecutable": "npm",
}, "runtimeArgs": ["run", "restart-dev-z7"]
{ },
"type": "node", {
"request": "launch", "type": "node",
"name": "Restart in Prod Mode", "request": "launch",
"runtimeExecutable": "npm", "name": "Reload",
"runtimeArgs": [ "runtimeExecutable": "npm",
"run", "runtimeArgs": ["run", "reload"]
"restart-prod" },
] {
} "type": "node",
] "request": "launch",
} "name": "Restart in Prod Mode",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "restart-prod"]
}
]
}

3
addon/bootstrap.js vendored
View File

@ -108,6 +108,9 @@ function shutdown({ id, version, resourceURI, rootURI }, reason) {
if (reason === APP_SHUTDOWN) { if (reason === APP_SHUTDOWN) {
return; return;
} }
if (reason == ADDON_DISABLE) {
Services.obs.notifyObservers(null, "startupcache-invalidate", null);
}
if (typeof Zotero === "undefined") { if (typeof Zotero === "undefined") {
Zotero = Components.classes["@zotero.org/Zotero;1"].getService( Zotero = Components.classes["@zotero.org/Zotero;1"].getService(
Components.interfaces.nsISupports Components.interfaces.nsISupports

View File

@ -13,19 +13,22 @@
}, },
"main": "src/index.ts", "main": "src/index.ts",
"scripts": { "scripts": {
"build-dev": "cross-env NODE_ENV=development 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.js", "build-prod": "cross-env NODE_ENV=production node scripts/build.mjs",
"build": "concurrently -c auto npm:build-prod npm:tsc", "build": "concurrently -c auto npm:build-prod npm:tsc",
"tsc": "tsc --noEmit", "tsc": "tsc --noEmit",
"start-z6": "node scripts/start.js --z 6", "start-z6": "node scripts/start.mjs --z 6",
"start-z7": "node scripts/start.js --z 7", "start-z7": "node scripts/start.mjs --z 7",
"start": "node scripts/start.js", "start": "node scripts/start.mjs",
"stop": "node scripts/stop.js", "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-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-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-dev": "npm run build-dev && npm run stop && npm run start",
"restart-prod": "npm run build-prod && npm run stop && npm run start", "restart-prod": "npm run build-prod && npm run stop && npm run start",
"restart": "npm run restart-dev", "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", "release": "release-it",
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"update-deps": "npm update --save" "update-deps": "npm update --save"
@ -73,6 +76,7 @@
"@types/node": "^20.3.0", "@types/node": "^20.3.0",
"@types/seedrandom": "^3.0.5", "@types/seedrandom": "^3.0.5",
"@types/yamljs": "^0.2.31", "@types/yamljs": "^0.2.31",
"chokidar-cli": "^3.0.0",
"compressing": "^1.9.0", "compressing": "^1.9.0",
"concurrently": "^7.6.0", "concurrently": "^7.6.0",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",

View File

@ -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);
});

192
scripts/build.mjs Normal file
View File

@ -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);
});

42
scripts/reload.mjs Normal file
View File

@ -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);

View File

@ -1,9 +1,12 @@
const { execSync } = require("child_process"); import process from "process";
const { exit } = require("process"); import { execSync } from "child_process";
const { exec } = require("./zotero-cmd.json"); 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 // 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) { if (args.help || args.h) {
console.log("Start Zotero Args:"); console.log("Start Zotero Args:");

View File

@ -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) {}

14
scripts/stop.mjs Normal file
View File

@ -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);
}