update: plugin template
This commit is contained in:
parent
c3a7755dfc
commit
c474f50435
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -1,4 +1,8 @@
|
|||
**/build
|
||||
build
|
||||
logs
|
||||
node_modules
|
||||
package-lock.json
|
||||
zotero-cmd.json
|
||||
pnpm-lock.yaml
|
||||
yarn.lock
|
||||
zotero-cmd.json
|
||||
.DS_Store
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
build
|
||||
logs
|
||||
node_modules
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
pnpm-lock.yaml
|
||||
# zotero-cmd.json
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"tabWidth": 2
|
||||
}
|
||||
|
|
@ -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}."
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
"applications": {
|
||||
"zotero": {
|
||||
"id": "__addonID__",
|
||||
"update_url": "__updaterdf__",
|
||||
"update_url": "__updateURL__",
|
||||
"strict_min_version": "6.999",
|
||||
"strict_max_version": "7.0.*"
|
||||
}
|
||||
|
|
|
|||
102
package.json
102
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}."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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);
|
||||
}
|
||||
})()`;
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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 <profile>/prefs.js has been modified.");
|
||||
Logger.debug("The <profile>/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();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"addons": {
|
||||
"__addonID__": {
|
||||
"updates": [
|
||||
{
|
||||
"version": "__buildVersion__",
|
||||
"update_link": "__updateLink__",
|
||||
"applications": {
|
||||
"zotero": {
|
||||
"strict_min_version": "6.999"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:em="http://www.mozilla.org/2004/em-rdf#">
|
||||
<rdf:Description rdf:about="urn:mozilla:extension:__addonID__">
|
||||
<em:updates>
|
||||
<rdf:Seq>
|
||||
<rdf:li>
|
||||
<rdf:Description>
|
||||
<em:version>1.0.4</em:version>
|
||||
<em:targetApplication>
|
||||
<rdf:Description>
|
||||
<em:id>zotero@chnm.gmu.edu</em:id>
|
||||
<em:minVersion>5.999</em:minVersion>
|
||||
<em:maxVersion>*</em:maxVersion>
|
||||
<em:updateLink>https://github.com/windingwind/zotero-better-notes/releases/download/1.0.4/zotero-better-notes.xpi</em:updateLink>
|
||||
</rdf:Description>
|
||||
</em:targetApplication>
|
||||
<em:targetApplication>
|
||||
<rdf:Description>
|
||||
<em:id>juris-m@juris-m.github.io</em:id>
|
||||
<em:minVersion>5.999</em:minVersion>
|
||||
<em:maxVersion>*</em:maxVersion>
|
||||
<em:updateLink>https://github.com/windingwind/zotero-better-notes/releases/download/1.0.4/zotero-better-notes.xpi</em:updateLink>
|
||||
</rdf:Description>
|
||||
</em:targetApplication>
|
||||
</rdf:Description>
|
||||
</rdf:li>
|
||||
</rdf:Seq>
|
||||
</em:updates>
|
||||
</rdf:Description>
|
||||
</rdf:RDF>
|
||||
Loading…
Reference in New Issue