zotero-plugin-scaffold 0.2.0-beta.13 → 0.2.0-beta.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,7 +4,7 @@ import process from 'node:process';
4
4
  import { Octokit } from 'octokit';
5
5
  import { globSync, glob } from 'tinyglobby';
6
6
  import { isCI } from 'std-env';
7
- import { B as Base } from '../shared/zotero-plugin-scaffold.CnLqPvW1.mjs';
7
+ import { B as Base } from '../shared/zotero-plugin-scaffold.rCsToUj_.mjs';
8
8
  import 'c12';
9
9
  import 'es-toolkit';
10
10
  import 'fs-extra/esm';
@@ -12,10 +12,10 @@ import 'hookable';
12
12
  import 'node:readline';
13
13
  import 'chalk';
14
14
  import '../shared/zotero-plugin-scaffold.v8dJZZ-v.mjs';
15
+ import 'node:fs';
15
16
  import 'adm-zip';
16
17
  import 'esbuild';
17
18
  import 'node:crypto';
18
- import 'node:fs';
19
19
  import 'node:child_process';
20
20
  import 'bumpp';
21
21
  import 'chokidar';
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import process from 'node:process';
2
2
  import { Command } from '@commander-js/extra-typings';
3
- import { E as ExitSignals, l as logger, C as Config, a as Build, S as Serve, T as Test, R as Release } from './shared/zotero-plugin-scaffold.CnLqPvW1.mjs';
3
+ import { E as ExitSignals, l as logger, C as Config, a as Build, S as Serve, T as Test, R as Release } from './shared/zotero-plugin-scaffold.rCsToUj_.mjs';
4
4
  import { readFile, writeFile } from 'node:fs/promises';
5
5
  import { pathExists, ensureFile } from 'fs-extra';
6
6
  import tinyUpdateNotifier from 'tiny-update-notifier';
@@ -13,11 +13,11 @@ import 'chalk';
13
13
  import 'std-env';
14
14
  import './shared/zotero-plugin-scaffold.v8dJZZ-v.mjs';
15
15
  import 'tinyglobby';
16
+ import 'node:fs';
16
17
  import 'node:path';
17
18
  import 'adm-zip';
18
19
  import 'esbuild';
19
20
  import 'node:crypto';
20
- import 'node:fs';
21
21
  import 'node:child_process';
22
22
  import 'bumpp';
23
23
  import 'chokidar';
@@ -30,8 +30,8 @@ import 'xvfb-ts';
30
30
 
31
31
  const name = "zotero-plugin-scaffold";
32
32
  const type = "module";
33
- const version = "0.2.0-beta.13";
34
- const packageManager = "pnpm@9.15.1";
33
+ const version = "0.2.0-beta.14";
34
+ const packageManager = "pnpm@9.15.2";
35
35
  const description = "A scaffold for Zotero plugin development.";
36
36
  const author = "northword";
37
37
  const license = "AGPL-3.0-or-later";
@@ -98,13 +98,13 @@ const peerDependenciesMeta = {
98
98
  }
99
99
  };
100
100
  const dependencies = {
101
- "@commander-js/extra-typings": "^12.1.0",
101
+ "@commander-js/extra-typings": "^13.0.0",
102
102
  "adm-zip": "^0.5.16",
103
103
  bumpp: "^9.9.2",
104
104
  c12: "^2.0.1",
105
105
  chalk: "^5.4.1",
106
106
  chokidar: "^4.0.3",
107
- commander: "^12.1.0",
107
+ commander: "^13.0.0",
108
108
  "es-toolkit": "^1.31.0",
109
109
  esbuild: "^0.24.2",
110
110
  "fs-extra": "^11.2.0",
package/dist/index.d.mts CHANGED
@@ -301,6 +301,40 @@ interface BuildConfig {
301
301
  */
302
302
  prefixFluentMessages: boolean;
303
303
  };
304
+ prefs: {
305
+ /**
306
+ * Prefixes the keys of preferences in `prefs.js` and
307
+ * the value of the `preference` attribute in XHTML files.
308
+ *
309
+ * 为 `prefs.js` 中的首选项键和 XHTML 文件中的 `preference` 属性的值
310
+ * 添加前缀。
311
+ *
312
+ * @default true
313
+ */
314
+ prefixPrefKeys: boolean;
315
+ /**
316
+ * Prefixes for Preference Keys, no dot at the end.
317
+ *
318
+ * 首选项键的前缀,结尾不需要加点号。
319
+ *
320
+ * @default 'extensions.${namespace}'
321
+ */
322
+ prefix: string;
323
+ /**
324
+ * Generate prefs.d.ts form prefs.js.
325
+ *
326
+ * - false: disable
327
+ * - string: path of dts file
328
+ *
329
+ * 为首选项生成类型声明文件。
330
+ *
331
+ * - false: 禁用
332
+ * - string:dts 文件路径
333
+ *
334
+ * @default 'typings/prefs.d.ts'
335
+ */
336
+ dts: false | string;
337
+ };
304
338
  /**
305
339
  * Configurations of esbuild.
306
340
  *
@@ -426,6 +460,8 @@ interface ServerConfig {
426
460
  * @default false
427
461
  */
428
462
  asProxy: boolean;
463
+ prebuild: boolean;
464
+ createProfileIfMissing: boolean;
429
465
  /**
430
466
  * The lifecycle hook.
431
467
  */
@@ -726,6 +762,7 @@ interface Hooks extends BuildHooks, ServeHooks, ReleaseHooks, TestHooks {
726
762
  type RecursivePartial<T> = {
727
763
  [P in keyof T]?: T[P] extends object ? RecursivePartial<T[P]> : T[P];
728
764
  };
765
+
729
766
  /**
730
767
  * User config
731
768
  */
@@ -789,6 +826,7 @@ declare class Build extends Base {
789
826
  */
790
827
  makeManifest(): Promise<void>;
791
828
  prepareLocaleFiles(): Promise<void>;
829
+ preparePrefs(): Promise<void>;
792
830
  esbuild(): Promise<esbuild.BuildResult<esbuild.BuildOptions>[]> | undefined;
793
831
  makeUpdateJson(): Promise<void>;
794
832
  pack(): Promise<void>;
@@ -816,7 +854,6 @@ declare class Serve extends Base {
816
854
  private builder;
817
855
  private runner?;
818
856
  private _zoteroBinPath?;
819
- private _profilePath?;
820
857
  constructor(ctx: Context);
821
858
  run(): Promise<void>;
822
859
  /**
@@ -828,8 +865,8 @@ declare class Serve extends Base {
828
865
  exit: () => never;
829
866
  private onZoteroExit;
830
867
  get zoteroBinPath(): string;
831
- get profilePath(): string;
832
- get dataDir(): string;
868
+ get profilePath(): string | undefined;
869
+ get dataDir(): string | undefined;
833
870
  }
834
871
 
835
872
  declare class Test extends Base {
package/dist/index.d.ts CHANGED
@@ -301,6 +301,40 @@ interface BuildConfig {
301
301
  */
302
302
  prefixFluentMessages: boolean;
303
303
  };
304
+ prefs: {
305
+ /**
306
+ * Prefixes the keys of preferences in `prefs.js` and
307
+ * the value of the `preference` attribute in XHTML files.
308
+ *
309
+ * 为 `prefs.js` 中的首选项键和 XHTML 文件中的 `preference` 属性的值
310
+ * 添加前缀。
311
+ *
312
+ * @default true
313
+ */
314
+ prefixPrefKeys: boolean;
315
+ /**
316
+ * Prefixes for Preference Keys, no dot at the end.
317
+ *
318
+ * 首选项键的前缀,结尾不需要加点号。
319
+ *
320
+ * @default 'extensions.${namespace}'
321
+ */
322
+ prefix: string;
323
+ /**
324
+ * Generate prefs.d.ts form prefs.js.
325
+ *
326
+ * - false: disable
327
+ * - string: path of dts file
328
+ *
329
+ * 为首选项生成类型声明文件。
330
+ *
331
+ * - false: 禁用
332
+ * - string:dts 文件路径
333
+ *
334
+ * @default 'typings/prefs.d.ts'
335
+ */
336
+ dts: false | string;
337
+ };
304
338
  /**
305
339
  * Configurations of esbuild.
306
340
  *
@@ -426,6 +460,8 @@ interface ServerConfig {
426
460
  * @default false
427
461
  */
428
462
  asProxy: boolean;
463
+ prebuild: boolean;
464
+ createProfileIfMissing: boolean;
429
465
  /**
430
466
  * The lifecycle hook.
431
467
  */
@@ -726,6 +762,7 @@ interface Hooks extends BuildHooks, ServeHooks, ReleaseHooks, TestHooks {
726
762
  type RecursivePartial<T> = {
727
763
  [P in keyof T]?: T[P] extends object ? RecursivePartial<T[P]> : T[P];
728
764
  };
765
+
729
766
  /**
730
767
  * User config
731
768
  */
@@ -789,6 +826,7 @@ declare class Build extends Base {
789
826
  */
790
827
  makeManifest(): Promise<void>;
791
828
  prepareLocaleFiles(): Promise<void>;
829
+ preparePrefs(): Promise<void>;
792
830
  esbuild(): Promise<esbuild.BuildResult<esbuild.BuildOptions>[]> | undefined;
793
831
  makeUpdateJson(): Promise<void>;
794
832
  pack(): Promise<void>;
@@ -816,7 +854,6 @@ declare class Serve extends Base {
816
854
  private builder;
817
855
  private runner?;
818
856
  private _zoteroBinPath?;
819
- private _profilePath?;
820
857
  constructor(ctx: Context);
821
858
  run(): Promise<void>;
822
859
  /**
@@ -828,8 +865,8 @@ declare class Serve extends Base {
828
865
  exit: () => never;
829
866
  private onZoteroExit;
830
867
  get zoteroBinPath(): string;
831
- get profilePath(): string;
832
- get dataDir(): string;
868
+ get profilePath(): string | undefined;
869
+ get dataDir(): string | undefined;
833
870
  }
834
871
 
835
872
  declare class Test extends Base {
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { a as Build, C as Config, R as Release, S as Serve, T as Test, d as defineConfig } from './shared/zotero-plugin-scaffold.CnLqPvW1.mjs';
1
+ export { a as Build, C as Config, R as Release, S as Serve, T as Test, d as defineConfig } from './shared/zotero-plugin-scaffold.rCsToUj_.mjs';
2
2
  import 'c12';
3
3
  import 'es-toolkit';
4
4
  import 'fs-extra/esm';
@@ -10,11 +10,11 @@ import 'std-env';
10
10
  import './shared/zotero-plugin-scaffold.v8dJZZ-v.mjs';
11
11
  import 'node:fs/promises';
12
12
  import 'tinyglobby';
13
+ import 'node:fs';
13
14
  import 'node:path';
14
15
  import 'adm-zip';
15
16
  import 'esbuild';
16
17
  import 'node:crypto';
17
- import 'node:fs';
18
18
  import 'node:child_process';
19
19
  import 'bumpp';
20
20
  import 'chokidar';
@@ -1,19 +1,19 @@
1
1
  import { loadConfig as loadConfig$1 } from 'c12';
2
- import { isPlainObject, kebabCase, mapValues, toMerged, escapeRegExp, delay, debounce } from 'es-toolkit';
3
- import { readJsonSync, emptyDir, copy, readJSON, outputJSON, move, writeJson, pathExists, outputFile, remove } from 'fs-extra/esm';
2
+ import { isPlainObject, kebabCase, mapValues, isNotNil, toMerged, escapeRegExp, delay, debounce } from 'es-toolkit';
3
+ import { readJsonSync, outputFile, emptyDir, copy, readJSON, outputJSON, move, writeJson, pathExists, ensureDir, remove } from 'fs-extra/esm';
4
4
  import { createHooks } from 'hookable';
5
5
  import process from 'node:process';
6
6
  import readline from 'node:readline';
7
7
  import chalk from 'chalk';
8
8
  import { isDebug, isCI, isWindows, isMacOS, isLinux } from 'std-env';
9
9
  import { p as parseRepoUrl, d as dateFormat, t as template, b as toArray, a as replaceInFile } from './zotero-plugin-scaffold.v8dJZZ-v.mjs';
10
+ import { createReadStream, existsSync } from 'node:fs';
10
11
  import { readFile, writeFile } from 'node:fs/promises';
11
12
  import { basename, dirname, join, resolve } from 'node:path';
12
13
  import AdmZip from 'adm-zip';
13
14
  import { build } from 'esbuild';
14
15
  import { glob } from 'tinyglobby';
15
16
  import { createHash } from 'node:crypto';
16
- import { createReadStream, existsSync } from 'node:fs';
17
17
  import { execSync, spawn } from 'node:child_process';
18
18
  import { versionBump, ProgressEvent } from 'bumpp';
19
19
  import chokidar from 'chokidar';
@@ -146,6 +146,7 @@ function resolveConfig(config) {
146
146
  config.id ||= config.name;
147
147
  config.namespace ||= config.name;
148
148
  config.xpiName ||= kebabCase(config.name);
149
+ config.build.prefs.prefix ||= `extensions.${config.namespace}`;
149
150
  const isPreRelease = version.includes("-");
150
151
  const templateData = {
151
152
  owner,
@@ -192,6 +193,11 @@ const defaultConfig = {
192
193
  prefixFluentMessages: true,
193
194
  prefixLocaleFiles: true
194
195
  },
196
+ prefs: {
197
+ prefix: "",
198
+ prefixPrefKeys: true,
199
+ dts: "typings/prefs.d.ts"
200
+ },
195
201
  esbuildOptions: [],
196
202
  makeManifest: {
197
203
  enable: true,
@@ -231,6 +237,9 @@ const defaultConfig = {
231
237
  devtools: true,
232
238
  startArgs: [],
233
239
  asProxy: false,
240
+ prebuild: true,
241
+ // keepProfileChanges: true,
242
+ createProfileIfMissing: true,
234
243
  hooks: {}
235
244
  },
236
245
  addonLint: {},
@@ -301,6 +310,109 @@ function generateHash(filePath, algorithm) {
301
310
  });
302
311
  }
303
312
 
313
+ class PrefsManager {
314
+ namespace;
315
+ prefs = {};
316
+ constructor(namespace) {
317
+ this.namespace = namespace;
318
+ }
319
+ parse(content) {
320
+ const prefPattern = /^(pref|user_pref)\s*\(\s*["']([^"']+)["']\s*,\s*(.+)\s*\)\s*;$/gm;
321
+ const matches = content.matchAll(prefPattern);
322
+ for (const match of matches) {
323
+ const key = match[2].trim();
324
+ const value = match[3].trim();
325
+ this.prefs[key] = value;
326
+ }
327
+ }
328
+ render() {
329
+ return Object.entries(this.prefs).map(([key, value]) => {
330
+ if (!isNotNil(value))
331
+ return "";
332
+ let cleanValue = "";
333
+ if (typeof value === "boolean") {
334
+ cleanValue = `${value}`;
335
+ } else if (typeof value === "string") {
336
+ cleanValue = value;
337
+ } else if (typeof value === "number") {
338
+ cleanValue = value.toString();
339
+ }
340
+ return `${this.namespace}("${key}", ${cleanValue});`;
341
+ }).filter((c) => !!c).join("\n");
342
+ }
343
+ async read(path) {
344
+ const content = await readFile(path, "utf-8");
345
+ this.parse(content);
346
+ }
347
+ async write(path) {
348
+ const content = this.render();
349
+ await outputFile(path, content, "utf-8");
350
+ logger.debug("The <profile>/prefs.js has been modified.");
351
+ }
352
+ setPref(key, value) {
353
+ this.prefs[key] = value;
354
+ }
355
+ setPrefs(prefs) {
356
+ Object.entries(prefs).forEach(([key, value]) => {
357
+ this.setPref(key, value);
358
+ });
359
+ }
360
+ getPref(key) {
361
+ return this.prefs[key] ?? void 0;
362
+ }
363
+ getPrefs() {
364
+ return this.prefs;
365
+ }
366
+ clearPrefs() {
367
+ this.prefs = {};
368
+ }
369
+ getPrefsWithPrefix(prefix) {
370
+ const _prefs = {};
371
+ for (const pref in this.prefs) {
372
+ if (pref.startsWith(prefix))
373
+ _prefs[pref] = this.prefs[pref];
374
+ else
375
+ _prefs[`${prefix}.${pref}`] = this.prefs[pref];
376
+ }
377
+ return _prefs;
378
+ }
379
+ getPrefsWithoutPrefix(prefix) {
380
+ const _prefs = {};
381
+ for (const pref in this.prefs) {
382
+ _prefs[pref.replace(`${prefix}.`, "")] = this.prefs[pref];
383
+ }
384
+ return _prefs;
385
+ }
386
+ }
387
+ function renderPluginPrefsDts(prefs, prefix) {
388
+ const dtsContent = `
389
+ /* prettier-ignore */
390
+ /* eslint-disable */
391
+ // @ts-nocheck
392
+ // Generated by zotero-plugin-scaffold
393
+
394
+ type _PluginPrefsMap = {
395
+ ${Object.entries(prefs).map(([key, value]) => {
396
+ return `"${key}": ${typeof value};`;
397
+ }).join("\n ")}
398
+ };
399
+
400
+ type PluginPrefKey<K extends keyof _PluginPrefsMap> = \`${prefix}.\${K}\`;
401
+
402
+ type PluginPrefsMap = {
403
+ [K in keyof _PluginPrefsMap as PluginPrefKey<K>]: _PluginPrefsMap[K]
404
+ };
405
+
406
+ declare namespace _ZoteroTypes {
407
+ interface Prefs {
408
+ get: <K extends keyof PluginPrefsMap>(key: K, global?: boolean) => PluginPrefsMap[K];
409
+ set: <K extends keyof PluginPrefsMap>(key: K, value: PluginPrefsMap[K], global?: boolean) => any;
410
+ }
411
+ }
412
+ `;
413
+ return dtsContent;
414
+ }
415
+
304
416
  class Base {
305
417
  ctx;
306
418
  constructor(ctx) {
@@ -342,6 +454,7 @@ class Build extends Base {
342
454
  this.logger.debug("Preparing locale files");
343
455
  await this.prepareLocaleFiles();
344
456
  await this.ctx.hooks.callHook("build:fluent", this.ctx);
457
+ await this.preparePrefs();
345
458
  this.logger.tip("Bundling scripts");
346
459
  await this.esbuild();
347
460
  await this.ctx.hooks.callHook("build:bundle", this.ctx);
@@ -472,6 +585,48 @@ class Build extends Base {
472
585
  }
473
586
  });
474
587
  }
588
+ async preparePrefs() {
589
+ const { dts, prefixPrefKeys, prefix } = this.ctx.build.prefs;
590
+ const { dist } = this.ctx;
591
+ if (!prefixPrefKeys && !dts)
592
+ return;
593
+ const prefsFilePath = join(dist, "addon", "prefs.js");
594
+ if (!existsSync(prefsFilePath))
595
+ return;
596
+ const prefsManager = new PrefsManager("pref");
597
+ await prefsManager.read(prefsFilePath);
598
+ const prefsWithPrefix = prefsManager.getPrefsWithPrefix(prefix);
599
+ const prefsWithoutPrefix = prefsManager.getPrefsWithoutPrefix(prefix);
600
+ if (dts) {
601
+ const dtsContent = renderPluginPrefsDts(prefsWithoutPrefix, prefix);
602
+ await outputFile(dts, dtsContent, "utf-8");
603
+ }
604
+ if (prefixPrefKeys) {
605
+ prefsManager.clearPrefs();
606
+ prefsManager.setPrefs(prefsWithPrefix);
607
+ await prefsManager.write(prefsFilePath);
608
+ }
609
+ if (prefixPrefKeys) {
610
+ const HTML_PREFERENCE_PATTERN = new RegExp(`preference="((?!${prefix})\\S*)"`, "g");
611
+ const xhtmlPaths = await glob(`${dist}/addon/**/*.xhtml`);
612
+ await Promise.all(xhtmlPaths.map(async (path) => {
613
+ let content = await readFile(path, "utf-8");
614
+ const matchs = [...content.matchAll(HTML_PREFERENCE_PATTERN)];
615
+ for (const match of matchs) {
616
+ const [matched, key] = match;
617
+ if (!prefsWithoutPrefix[key] && !prefsWithoutPrefix[key]) {
618
+ this.logger.warn(`preference key '${key}' in ${path} not init in prefs.js`);
619
+ continue;
620
+ }
621
+ if (key.startsWith(prefix))
622
+ continue;
623
+ else
624
+ content = content.replace(matched, `preference="${prefix}.${key}"`);
625
+ }
626
+ await outputFile(path, content, "utf-8");
627
+ }));
628
+ }
629
+ }
475
630
  esbuild() {
476
631
  const { build: { esbuildOptions } } = this.ctx;
477
632
  if (esbuildOptions.length === 0)
@@ -1176,18 +1331,43 @@ function findFreeTcpPort() {
1176
1331
  });
1177
1332
  }
1178
1333
 
1334
+ const default_options = {
1335
+ binary: {
1336
+ // path: "",
1337
+ args: [],
1338
+ devtools: true
1339
+ },
1340
+ profile: {
1341
+ path: "./.scaffold/profile",
1342
+ dataDir: "",
1343
+ // keepChanges: true,
1344
+ createIfMissing: true,
1345
+ customPrefs: {}
1346
+ },
1347
+ plugins: {
1348
+ asProxy: false,
1349
+ list: []
1350
+ }
1351
+ };
1179
1352
  class ZoteroRunner {
1180
1353
  options;
1181
- remoteFirefox;
1354
+ remoteFirefox = new RemoteFirefox();
1182
1355
  zotero;
1183
1356
  constructor(options) {
1184
- this.options = options;
1185
- this.remoteFirefox = new RemoteFirefox();
1357
+ this.options = toMerged(default_options, options);
1358
+ if (!options.binary.path)
1359
+ throw new Error("Binary path must be provided.");
1360
+ if (!options.profile.path && !options.profile.dataDir)
1361
+ this.options.profile.dataDir = "./.scaffold/data";
1362
+ logger.debug(this.options);
1363
+ }
1364
+ get default_profile_path() {
1365
+ return default_options.profile.path;
1186
1366
  }
1187
1367
  async run() {
1188
- await this.setupProfileDir();
1368
+ await this.setupProfile();
1189
1369
  await this.startZoteroInstance();
1190
- if (!this.options.asProxy)
1370
+ if (!this.options.plugins.asProxy)
1191
1371
  await this.installTemporaryPlugins();
1192
1372
  }
1193
1373
  /**
@@ -1197,45 +1377,51 @@ class ZoteroRunner {
1197
1377
  *
1198
1378
  * @see https://www.zotero.org/support/dev/client_coding/plugin_development#setting_up_a_plugin_development_environment
1199
1379
  */
1200
- async setupProfileDir() {
1201
- if (!this.options.profilePath) ;
1202
- const defaultPrefs = Object.entries(prefs).map(([key, value]) => {
1203
- return `user_pref("${key}", ${JSON.stringify(value)});`;
1204
- });
1205
- const customPrefs = Object.entries(this.options.customPrefs || []).map(([key, value]) => {
1206
- return `user_pref("${key}", ${JSON.stringify(value)});`;
1207
- });
1208
- let exsitedPrefs = [];
1209
- const prefsPath = join(this.options.profilePath, "prefs.js");
1210
- if (await pathExists(prefsPath)) {
1211
- const PrefsLines = (await readFile(prefsPath, "utf-8")).split("\n");
1212
- exsitedPrefs = PrefsLines.map((line) => {
1213
- if (line.includes("extensions.lastAppBuildId") || line.includes("extensions.lastAppVersion")) {
1214
- return "";
1215
- }
1216
- return line;
1217
- });
1380
+ async setupProfile() {
1381
+ const { path, createIfMissing } = this.options.profile;
1382
+ if (!await pathExists(path)) {
1383
+ if (createIfMissing)
1384
+ await this.createProfile(this.default_profile_path);
1385
+ else
1386
+ throw new Error("The 'profile.path' must be provided when 'createIfMissing' is false.");
1218
1387
  }
1219
- const updatedPrefs = [...defaultPrefs, ...exsitedPrefs, ...customPrefs].join("\n");
1220
- await outputFile(prefsPath, updatedPrefs, "utf-8");
1221
- logger.debug("The <profile>/prefs.js has been modified.");
1222
- if (this.options.asProxy) {
1388
+ const prefsPath = join(this.options.profile.path, "prefs.js");
1389
+ const prefsManager = new PrefsManager("user_pref");
1390
+ prefsManager.setPrefs(prefs);
1391
+ prefsManager.setPrefs(this.options.profile.customPrefs);
1392
+ if (await pathExists(prefsPath))
1393
+ await prefsManager.read(prefsPath);
1394
+ prefsManager.setPrefs({
1395
+ "extensions.lastAppBuildId": null,
1396
+ "extensions.lastAppVersion": null
1397
+ });
1398
+ await prefsManager.write(prefsPath);
1399
+ if (this.options.plugins.asProxy) {
1223
1400
  await this.installProxyPlugins();
1224
1401
  }
1225
1402
  }
1403
+ async createProfile(path) {
1404
+ logger.debug(`Creating profile at ${this.default_profile_path}...`);
1405
+ await ensureDir(path);
1406
+ }
1407
+ // private async copyProfile(from: string, to = this.default_profile_path) {
1408
+ // logger.debug(`Copying profile from '${this.options.profile.path}' to ${this.default_profile_path}...`);
1409
+ // await copy(from, to);
1410
+ // this.options.profile.path = to;
1411
+ // }
1226
1412
  async startZoteroInstance() {
1227
1413
  let args = ["--purgecaches", "no-remote"];
1228
- if (this.options.profilePath) {
1229
- args.push("-profile", resolve(this.options.profilePath));
1414
+ if (this.options.profile.path) {
1415
+ args.push("-profile", resolve(this.options.profile.path));
1230
1416
  }
1231
- if (this.options.dataDir) {
1232
- args.push("--dataDir", resolve(this.options.dataDir));
1417
+ if (this.options.profile.dataDir) {
1418
+ args.push("--dataDir", resolve(this.options.profile.dataDir));
1233
1419
  }
1234
- if (this.options.devtools) {
1420
+ if (this.options.binary.devtools) {
1235
1421
  args.push("--jsdebugger");
1236
1422
  }
1237
- if (this.options.binaryArgs) {
1238
- args = [...args, ...this.options.binaryArgs];
1423
+ if (this.options.binary.args) {
1424
+ args = [...args, ...this.options.binary.args];
1239
1425
  }
1240
1426
  const remotePort = await findFreeTcpPort();
1241
1427
  args.push("-start-debugger-server", String(remotePort));
@@ -1245,7 +1431,9 @@ class ZoteroRunner {
1245
1431
  XPCOM_DEBUG_BREAK: "stack",
1246
1432
  NS_TRACE_MALLOC_DISABLE_STACKS: "1"
1247
1433
  };
1248
- this.zotero = spawn(this.options.binaryPath, args, { env });
1434
+ if (!await pathExists(this.options.binary.path))
1435
+ throw new Error("The Zotero binary not found.");
1436
+ this.zotero = spawn(this.options.binary.path, args, { env });
1249
1437
  logger.debug("Zotero started, pid:", this.zotero.pid);
1250
1438
  this.zotero.stdout?.on("data", (_data) => {
1251
1439
  });
@@ -1254,7 +1442,7 @@ class ZoteroRunner {
1254
1442
  logger.debug(`Connected to the remote Firefox debugger on port: ${remotePort}`);
1255
1443
  }
1256
1444
  async installTemporaryPlugins() {
1257
- for (const plugin of this.options.plugins) {
1445
+ for (const plugin of this.options.plugins.list) {
1258
1446
  const addonId = await this.remoteFirefox.installTemporaryAddon(resolve(plugin.sourceDir)).then((installResult) => {
1259
1447
  return installResult.addon.id;
1260
1448
  });
@@ -1264,7 +1452,7 @@ class ZoteroRunner {
1264
1452
  }
1265
1453
  }
1266
1454
  async installProxyPlugin(id, sourceDir) {
1267
- const addonProxyFilePath = join(this.options.profilePath, `extensions/${id}`);
1455
+ const addonProxyFilePath = join(this.options.profile.path, `extensions/${id}`);
1268
1456
  const buildPath = resolve(sourceDir);
1269
1457
  await outputFile(addonProxyFilePath, buildPath);
1270
1458
  logger.debug(
@@ -1274,12 +1462,12 @@ class ZoteroRunner {
1274
1462
  ` Addon path: ${buildPath}`
1275
1463
  ].join("\n")
1276
1464
  );
1277
- const addonXpiFilePath = join(this.options.profilePath, `extensions/${id}.xpi`);
1465
+ const addonXpiFilePath = join(this.options.profile.path, `extensions/${id}.xpi`);
1278
1466
  if (await pathExists(addonXpiFilePath)) {
1279
1467
  await remove(addonXpiFilePath);
1280
1468
  logger.debug(`XPI file found, removed.`);
1281
1469
  }
1282
- const addonInfoFilePath = join(this.options.profilePath, "extensions.json");
1470
+ const addonInfoFilePath = join(this.options.profile.path, "extensions.json");
1283
1471
  if (await pathExists(addonInfoFilePath)) {
1284
1472
  const content = await readJSON(addonInfoFilePath);
1285
1473
  content.addons = content.addons.map((addon) => {
@@ -1294,7 +1482,7 @@ class ZoteroRunner {
1294
1482
  }
1295
1483
  }
1296
1484
  async installProxyPlugins() {
1297
- for (const { id, sourceDir } of this.options.plugins) {
1485
+ for (const { id, sourceDir } of this.options.plugins.list) {
1298
1486
  await this.installProxyPlugin(id, sourceDir);
1299
1487
  }
1300
1488
  }
@@ -1302,7 +1490,7 @@ class ZoteroRunner {
1302
1490
  await this.remoteFirefox.reloadAddon(id);
1303
1491
  }
1304
1492
  async reloadTemporaryPluginBySourceDir(sourceDir) {
1305
- const addonId = this.options.plugins.find((p) => p.sourceDir === sourceDir)?.id;
1493
+ const addonId = this.options.plugins.list.find((p) => p.sourceDir === sourceDir)?.id;
1306
1494
  if (!addonId) {
1307
1495
  return {
1308
1496
  sourceDir,
@@ -1322,7 +1510,7 @@ class ZoteroRunner {
1322
1510
  return { sourceDir, reloadError: void 0 };
1323
1511
  }
1324
1512
  async reloadAllTemporaryPlugins() {
1325
- for (const { sourceDir } of this.options.plugins) {
1513
+ for (const { sourceDir } of this.options.plugins.list) {
1326
1514
  const res = await this.reloadTemporaryPluginBySourceDir(sourceDir);
1327
1515
  if (res.reloadError instanceof Error) {
1328
1516
  logger.error(res.reloadError);
@@ -1349,20 +1537,20 @@ class ZoteroRunner {
1349
1537
  const url = `zotero://ztoolkit-debug/?run=${encodeURIComponent(
1350
1538
  reloadScript
1351
1539
  )}`;
1352
- const startZoteroCmd = `"${this.options.binaryPath}" --purgecaches -profile "${this.options.profilePath}"`;
1540
+ const startZoteroCmd = `"${this.options.binary.path}" --purgecaches -profile "${this.options.profile.path}"`;
1353
1541
  const command = `${startZoteroCmd} -url "${url}"`;
1354
1542
  execSync(command);
1355
1543
  }
1356
1544
  // Do not use this method if possible,
1357
1545
  // as frequent execSync can cause Zotero to crash.
1358
1546
  async reloadAllProxyPlugins() {
1359
- for (const { id } of this.options.plugins) {
1547
+ for (const { id } of this.options.plugins.list) {
1360
1548
  await this.reloadProxyPluginByZToolkit(id, id, id);
1361
1549
  await delay(2e3);
1362
1550
  }
1363
1551
  }
1364
1552
  async reloadAllPlugins() {
1365
- if (this.options.asProxy)
1553
+ if (this.options.plugins.asProxy)
1366
1554
  await this.reloadAllProxyPlugins();
1367
1555
  else
1368
1556
  await this.reloadAllTemporaryPlugins();
@@ -1401,7 +1589,6 @@ class Serve extends Base {
1401
1589
  builder;
1402
1590
  runner;
1403
1591
  _zoteroBinPath;
1404
- _profilePath;
1405
1592
  constructor(ctx) {
1406
1593
  super(ctx);
1407
1594
  process.env.NODE_ENV ??= "development";
@@ -1410,20 +1597,30 @@ class Serve extends Base {
1410
1597
  async run() {
1411
1598
  process.on("SIGINT", this.exit);
1412
1599
  this.runner = new ZoteroRunner({
1413
- binaryPath: this.zoteroBinPath,
1414
- profilePath: this.profilePath,
1415
- dataDir: this.dataDir,
1416
- plugins: [{
1417
- id: this.ctx.id,
1418
- sourceDir: join(this.ctx.dist, "addon")
1419
- }],
1420
- asProxy: this.ctx.server.asProxy,
1421
- devtools: this.ctx.server.devtools,
1422
- binaryArgs: this.ctx.server.startArgs
1600
+ binary: {
1601
+ path: this.zoteroBinPath,
1602
+ devtools: this.ctx.server.devtools,
1603
+ args: this.ctx.server.startArgs
1604
+ },
1605
+ profile: {
1606
+ path: this.profilePath,
1607
+ dataDir: this.dataDir,
1608
+ // keepChanges: this.ctx.server.keepProfileChanges,
1609
+ createIfMissing: this.ctx.server.createProfileIfMissing
1610
+ },
1611
+ plugins: {
1612
+ list: [{
1613
+ id: this.ctx.id,
1614
+ sourceDir: join(this.ctx.dist, "addon")
1615
+ }],
1616
+ asProxy: this.ctx.server.asProxy
1617
+ }
1423
1618
  });
1424
1619
  await this.ctx.hooks.callHook("serve:init", this.ctx);
1425
- await this.builder.run();
1426
- await this.ctx.hooks.callHook("serve:prebuild", this.ctx);
1620
+ if (this.ctx.server.prebuild) {
1621
+ await this.builder.run();
1622
+ await this.ctx.hooks.callHook("serve:prebuild", this.ctx);
1623
+ }
1427
1624
  await this.runner.run();
1428
1625
  this.runner.zotero?.on("exit", this.onZoteroExit);
1429
1626
  this.runner.zotero?.on("close", this.onZoteroExit);
@@ -1490,15 +1687,10 @@ class Serve extends Base {
1490
1687
  return this._zoteroBinPath;
1491
1688
  }
1492
1689
  get profilePath() {
1493
- if (this._profilePath)
1494
- return this._profilePath;
1495
- this._profilePath = process.env.ZOTERO_PLUGIN_PROFILE_PATH;
1496
- if (!this._profilePath || !existsSync(this._profilePath))
1497
- throw new Error("The Zotero profile not found.");
1498
- return this._profilePath;
1690
+ return process.env.ZOTERO_PLUGIN_PROFILE_PATH;
1499
1691
  }
1500
1692
  get dataDir() {
1501
- return process.env.ZOTERO_PLUGIN_DATA_DIR ?? "";
1693
+ return process.env.ZOTERO_PLUGIN_DATA_DIR;
1502
1694
  }
1503
1695
  }
1504
1696
 
@@ -2036,19 +2228,25 @@ class Test extends Base {
2036
2228
  }
2037
2229
  async startZotero() {
2038
2230
  this.runner = new ZoteroRunner({
2039
- binaryPath: this.zoteroBinPath,
2040
- profilePath: this.profilePath,
2041
- dataDir: this.dataDir,
2042
- plugins: [{
2043
- id: this.ctx.id,
2044
- sourceDir: join(this.ctx.dist, "addon")
2045
- }, {
2046
- id: this.testPluginID,
2047
- sourceDir: this.testPluginDir
2048
- }],
2049
- devtools: this.ctx.server.devtools,
2050
- binaryArgs: this.ctx.server.startArgs,
2051
- customPrefs: this.prefs
2231
+ binary: {
2232
+ path: this.zoteroBinPath,
2233
+ devtools: this.ctx.server.devtools,
2234
+ args: this.ctx.server.startArgs
2235
+ },
2236
+ profile: {
2237
+ path: this.profilePath,
2238
+ dataDir: this.dataDir,
2239
+ customPrefs: this.prefs
2240
+ },
2241
+ plugins: {
2242
+ list: [{
2243
+ id: this.ctx.id,
2244
+ sourceDir: join(this.ctx.dist, "addon")
2245
+ }, {
2246
+ id: this.testPluginID,
2247
+ sourceDir: this.testPluginDir
2248
+ }]
2249
+ }
2052
2250
  });
2053
2251
  await this.runner.run();
2054
2252
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "zotero-plugin-scaffold",
3
3
  "type": "module",
4
- "version": "0.2.0-beta.13",
4
+ "version": "0.2.0-beta.14",
5
5
  "description": "A scaffold for Zotero plugin development.",
6
6
  "author": "northword",
7
7
  "license": "AGPL-3.0-or-later",
@@ -63,13 +63,13 @@
63
63
  }
64
64
  },
65
65
  "dependencies": {
66
- "@commander-js/extra-typings": "^12.1.0",
66
+ "@commander-js/extra-typings": "^13.0.0",
67
67
  "adm-zip": "^0.5.16",
68
68
  "bumpp": "^9.9.2",
69
69
  "c12": "^2.0.1",
70
70
  "chalk": "^5.4.1",
71
71
  "chokidar": "^4.0.3",
72
- "commander": "^12.1.0",
72
+ "commander": "^13.0.0",
73
73
  "es-toolkit": "^1.31.0",
74
74
  "esbuild": "^0.24.2",
75
75
  "fs-extra": "^11.2.0",