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

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.DQuI5OK-.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.DQuI5OK-.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.15";
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
@@ -51,14 +51,9 @@ interface Manifest {
51
51
  zotero: {
52
52
  id: string;
53
53
  update_url: string;
54
- strict_min_version: string;
54
+ strict_min_version?: string;
55
55
  strict_max_version?: string;
56
56
  };
57
- gecko: {
58
- id: string;
59
- update_url: string;
60
- strict_min_version: string;
61
- };
62
57
  };
63
58
  }
64
59
 
@@ -81,7 +76,7 @@ interface UpdateJSON {
81
76
  update_hash?: string;
82
77
  applications: {
83
78
  zotero: {
84
- strict_min_version: string;
79
+ strict_min_version?: string;
85
80
  strict_max_version?: string;
86
81
  };
87
82
  [application: string]: {
@@ -301,6 +296,40 @@ interface BuildConfig {
301
296
  */
302
297
  prefixFluentMessages: boolean;
303
298
  };
299
+ prefs: {
300
+ /**
301
+ * Prefixes the keys of preferences in `prefs.js` and
302
+ * the value of the `preference` attribute in XHTML files.
303
+ *
304
+ * 为 `prefs.js` 中的首选项键和 XHTML 文件中的 `preference` 属性的值
305
+ * 添加前缀。
306
+ *
307
+ * @default true
308
+ */
309
+ prefixPrefKeys: boolean;
310
+ /**
311
+ * Prefixes for Preference Keys, no dot at the end.
312
+ *
313
+ * 首选项键的前缀,结尾不需要加点号。
314
+ *
315
+ * @default 'extensions.${namespace}'
316
+ */
317
+ prefix: string;
318
+ /**
319
+ * Generate prefs.d.ts form prefs.js.
320
+ *
321
+ * - false: disable
322
+ * - string: path of dts file
323
+ *
324
+ * 为首选项生成类型声明文件。
325
+ *
326
+ * - false: 禁用
327
+ * - string:dts 文件路径
328
+ *
329
+ * @default 'typings/prefs.d.ts'
330
+ */
331
+ dts: false | string;
332
+ };
304
333
  /**
305
334
  * Configurations of esbuild.
306
335
  *
@@ -426,6 +455,8 @@ interface ServerConfig {
426
455
  * @default false
427
456
  */
428
457
  asProxy: boolean;
458
+ prebuild: boolean;
459
+ createProfileIfMissing: boolean;
429
460
  /**
430
461
  * The lifecycle hook.
431
462
  */
@@ -726,6 +757,7 @@ interface Hooks extends BuildHooks, ServeHooks, ReleaseHooks, TestHooks {
726
757
  type RecursivePartial<T> = {
727
758
  [P in keyof T]?: T[P] extends object ? RecursivePartial<T[P]> : T[P];
728
759
  };
760
+
729
761
  /**
730
762
  * User config
731
763
  */
@@ -789,6 +821,7 @@ declare class Build extends Base {
789
821
  */
790
822
  makeManifest(): Promise<void>;
791
823
  prepareLocaleFiles(): Promise<void>;
824
+ preparePrefs(): Promise<void>;
792
825
  esbuild(): Promise<esbuild.BuildResult<esbuild.BuildOptions>[]> | undefined;
793
826
  makeUpdateJson(): Promise<void>;
794
827
  pack(): Promise<void>;
@@ -816,7 +849,6 @@ declare class Serve extends Base {
816
849
  private builder;
817
850
  private runner?;
818
851
  private _zoteroBinPath?;
819
- private _profilePath?;
820
852
  constructor(ctx: Context);
821
853
  run(): Promise<void>;
822
854
  /**
@@ -828,8 +860,8 @@ declare class Serve extends Base {
828
860
  exit: () => never;
829
861
  private onZoteroExit;
830
862
  get zoteroBinPath(): string;
831
- get profilePath(): string;
832
- get dataDir(): string;
863
+ get profilePath(): string | undefined;
864
+ get dataDir(): string | undefined;
833
865
  }
834
866
 
835
867
  declare class Test extends Base {
package/dist/index.d.ts CHANGED
@@ -51,14 +51,9 @@ interface Manifest {
51
51
  zotero: {
52
52
  id: string;
53
53
  update_url: string;
54
- strict_min_version: string;
54
+ strict_min_version?: string;
55
55
  strict_max_version?: string;
56
56
  };
57
- gecko: {
58
- id: string;
59
- update_url: string;
60
- strict_min_version: string;
61
- };
62
57
  };
63
58
  }
64
59
 
@@ -81,7 +76,7 @@ interface UpdateJSON {
81
76
  update_hash?: string;
82
77
  applications: {
83
78
  zotero: {
84
- strict_min_version: string;
79
+ strict_min_version?: string;
85
80
  strict_max_version?: string;
86
81
  };
87
82
  [application: string]: {
@@ -301,6 +296,40 @@ interface BuildConfig {
301
296
  */
302
297
  prefixFluentMessages: boolean;
303
298
  };
299
+ prefs: {
300
+ /**
301
+ * Prefixes the keys of preferences in `prefs.js` and
302
+ * the value of the `preference` attribute in XHTML files.
303
+ *
304
+ * 为 `prefs.js` 中的首选项键和 XHTML 文件中的 `preference` 属性的值
305
+ * 添加前缀。
306
+ *
307
+ * @default true
308
+ */
309
+ prefixPrefKeys: boolean;
310
+ /**
311
+ * Prefixes for Preference Keys, no dot at the end.
312
+ *
313
+ * 首选项键的前缀,结尾不需要加点号。
314
+ *
315
+ * @default 'extensions.${namespace}'
316
+ */
317
+ prefix: string;
318
+ /**
319
+ * Generate prefs.d.ts form prefs.js.
320
+ *
321
+ * - false: disable
322
+ * - string: path of dts file
323
+ *
324
+ * 为首选项生成类型声明文件。
325
+ *
326
+ * - false: 禁用
327
+ * - string:dts 文件路径
328
+ *
329
+ * @default 'typings/prefs.d.ts'
330
+ */
331
+ dts: false | string;
332
+ };
304
333
  /**
305
334
  * Configurations of esbuild.
306
335
  *
@@ -426,6 +455,8 @@ interface ServerConfig {
426
455
  * @default false
427
456
  */
428
457
  asProxy: boolean;
458
+ prebuild: boolean;
459
+ createProfileIfMissing: boolean;
429
460
  /**
430
461
  * The lifecycle hook.
431
462
  */
@@ -726,6 +757,7 @@ interface Hooks extends BuildHooks, ServeHooks, ReleaseHooks, TestHooks {
726
757
  type RecursivePartial<T> = {
727
758
  [P in keyof T]?: T[P] extends object ? RecursivePartial<T[P]> : T[P];
728
759
  };
760
+
729
761
  /**
730
762
  * User config
731
763
  */
@@ -789,6 +821,7 @@ declare class Build extends Base {
789
821
  */
790
822
  makeManifest(): Promise<void>;
791
823
  prepareLocaleFiles(): Promise<void>;
824
+ preparePrefs(): Promise<void>;
792
825
  esbuild(): Promise<esbuild.BuildResult<esbuild.BuildOptions>[]> | undefined;
793
826
  makeUpdateJson(): Promise<void>;
794
827
  pack(): Promise<void>;
@@ -816,7 +849,6 @@ declare class Serve extends Base {
816
849
  private builder;
817
850
  private runner?;
818
851
  private _zoteroBinPath?;
819
- private _profilePath?;
820
852
  constructor(ctx: Context);
821
853
  run(): Promise<void>;
822
854
  /**
@@ -828,8 +860,8 @@ declare class Serve extends Base {
828
860
  exit: () => never;
829
861
  private onZoteroExit;
830
862
  get zoteroBinPath(): string;
831
- get profilePath(): string;
832
- get dataDir(): string;
863
+ get profilePath(): string | undefined;
864
+ get dataDir(): string | undefined;
833
865
  }
834
866
 
835
867
  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.DQuI5OK-.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,31 +193,22 @@ 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,
198
204
  template: {
199
205
  manifest_version: 2,
200
- name: "__addonName__",
201
- version: "__buildVersion__",
202
- description: "__description__",
203
- homepage_url: "__homepage__",
204
- author: "__author__",
205
- icons: {
206
- 48: "content/icons/favicon@0.5x.png",
207
- 96: "content/icons/favicon.png"
208
- },
206
+ name: "",
207
+ version: "",
209
208
  applications: {
210
209
  zotero: {
211
- id: "__addonID__",
212
- update_url: "__updateURL__",
213
- strict_min_version: "6.999",
214
- strict_max_version: "7.0.*"
215
- },
216
- gecko: {
217
- id: "__addonID__",
218
- update_url: "__updateURL__",
219
- strict_min_version: "102"
210
+ id: "",
211
+ update_url: ""
220
212
  }
221
213
  }
222
214
  }
@@ -231,6 +223,9 @@ const defaultConfig = {
231
223
  devtools: true,
232
224
  startArgs: [],
233
225
  asProxy: false,
226
+ prebuild: true,
227
+ // keepProfileChanges: true,
228
+ createProfileIfMissing: true,
234
229
  hooks: {}
235
230
  },
236
231
  addonLint: {},
@@ -301,6 +296,109 @@ function generateHash(filePath, algorithm) {
301
296
  });
302
297
  }
303
298
 
299
+ class PrefsManager {
300
+ namespace;
301
+ prefs = {};
302
+ constructor(namespace) {
303
+ this.namespace = namespace;
304
+ }
305
+ parse(content) {
306
+ const prefPattern = /^(pref|user_pref)\s*\(\s*["']([^"']+)["']\s*,\s*(.+)\s*\)\s*;$/gm;
307
+ const matches = content.matchAll(prefPattern);
308
+ for (const match of matches) {
309
+ const key = match[2].trim();
310
+ const value = match[3].trim();
311
+ this.prefs[key] = value;
312
+ }
313
+ }
314
+ render() {
315
+ return Object.entries(this.prefs).map(([key, value]) => {
316
+ if (!isNotNil(value))
317
+ return "";
318
+ let cleanValue = "";
319
+ if (typeof value === "boolean") {
320
+ cleanValue = `${value}`;
321
+ } else if (typeof value === "string") {
322
+ cleanValue = value;
323
+ } else if (typeof value === "number") {
324
+ cleanValue = value.toString();
325
+ }
326
+ return `${this.namespace}("${key}", ${cleanValue});`;
327
+ }).filter((c) => !!c).join("\n");
328
+ }
329
+ async read(path) {
330
+ const content = await readFile(path, "utf-8");
331
+ this.parse(content);
332
+ }
333
+ async write(path) {
334
+ const content = this.render();
335
+ await outputFile(path, content, "utf-8");
336
+ logger.debug("The <profile>/prefs.js has been modified.");
337
+ }
338
+ setPref(key, value) {
339
+ this.prefs[key] = value;
340
+ }
341
+ setPrefs(prefs) {
342
+ Object.entries(prefs).forEach(([key, value]) => {
343
+ this.setPref(key, value);
344
+ });
345
+ }
346
+ getPref(key) {
347
+ return this.prefs[key] ?? void 0;
348
+ }
349
+ getPrefs() {
350
+ return this.prefs;
351
+ }
352
+ clearPrefs() {
353
+ this.prefs = {};
354
+ }
355
+ getPrefsWithPrefix(prefix) {
356
+ const _prefs = {};
357
+ for (const pref in this.prefs) {
358
+ if (pref.startsWith(prefix))
359
+ _prefs[pref] = this.prefs[pref];
360
+ else
361
+ _prefs[`${prefix}.${pref}`] = this.prefs[pref];
362
+ }
363
+ return _prefs;
364
+ }
365
+ getPrefsWithoutPrefix(prefix) {
366
+ const _prefs = {};
367
+ for (const pref in this.prefs) {
368
+ _prefs[pref.replace(`${prefix}.`, "")] = this.prefs[pref];
369
+ }
370
+ return _prefs;
371
+ }
372
+ }
373
+ function renderPluginPrefsDts(prefs, prefix) {
374
+ const dtsContent = `
375
+ /* prettier-ignore */
376
+ /* eslint-disable */
377
+ // @ts-nocheck
378
+ // Generated by zotero-plugin-scaffold
379
+
380
+ type _PluginPrefsMap = {
381
+ ${Object.entries(prefs).map(([key, value]) => {
382
+ return `"${key}": ${typeof value};`;
383
+ }).join("\n ")}
384
+ };
385
+
386
+ type PluginPrefKey<K extends keyof _PluginPrefsMap> = \`${prefix}.\${K}\`;
387
+
388
+ type PluginPrefsMap = {
389
+ [K in keyof _PluginPrefsMap as PluginPrefKey<K>]: _PluginPrefsMap[K]
390
+ };
391
+
392
+ declare namespace _ZoteroTypes {
393
+ interface Prefs {
394
+ get: <K extends keyof PluginPrefsMap>(key: K, global?: boolean) => PluginPrefsMap[K];
395
+ set: <K extends keyof PluginPrefsMap>(key: K, value: PluginPrefsMap[K], global?: boolean) => any;
396
+ }
397
+ }
398
+ `;
399
+ return dtsContent;
400
+ }
401
+
304
402
  class Base {
305
403
  ctx;
306
404
  constructor(ctx) {
@@ -342,6 +440,7 @@ class Build extends Base {
342
440
  this.logger.debug("Preparing locale files");
343
441
  await this.prepareLocaleFiles();
344
442
  await this.ctx.hooks.callHook("build:fluent", this.ctx);
443
+ await this.preparePrefs();
345
444
  this.logger.tip("Bundling scripts");
346
445
  await this.esbuild();
347
446
  await this.ctx.hooks.callHook("build:bundle", this.ctx);
@@ -400,7 +499,6 @@ class Build extends Base {
400
499
  ...version && { version },
401
500
  manifest_version: 2,
402
501
  applications: {
403
- // @ts-expect-error 此处不包含版本限制
404
502
  zotero: {
405
503
  id,
406
504
  update_url: updateURL
@@ -472,6 +570,48 @@ class Build extends Base {
472
570
  }
473
571
  });
474
572
  }
573
+ async preparePrefs() {
574
+ const { dts, prefixPrefKeys, prefix } = this.ctx.build.prefs;
575
+ const { dist } = this.ctx;
576
+ if (!prefixPrefKeys && !dts)
577
+ return;
578
+ const prefsFilePath = join(dist, "addon", "prefs.js");
579
+ if (!existsSync(prefsFilePath))
580
+ return;
581
+ const prefsManager = new PrefsManager("pref");
582
+ await prefsManager.read(prefsFilePath);
583
+ const prefsWithPrefix = prefsManager.getPrefsWithPrefix(prefix);
584
+ const prefsWithoutPrefix = prefsManager.getPrefsWithoutPrefix(prefix);
585
+ if (dts) {
586
+ const dtsContent = renderPluginPrefsDts(prefsWithoutPrefix, prefix);
587
+ await outputFile(dts, dtsContent, "utf-8");
588
+ }
589
+ if (prefixPrefKeys) {
590
+ prefsManager.clearPrefs();
591
+ prefsManager.setPrefs(prefsWithPrefix);
592
+ await prefsManager.write(prefsFilePath);
593
+ }
594
+ if (prefixPrefKeys) {
595
+ const HTML_PREFERENCE_PATTERN = new RegExp(`preference="((?!${prefix})\\S*)"`, "g");
596
+ const xhtmlPaths = await glob(`${dist}/addon/**/*.xhtml`);
597
+ await Promise.all(xhtmlPaths.map(async (path) => {
598
+ let content = await readFile(path, "utf-8");
599
+ const matchs = [...content.matchAll(HTML_PREFERENCE_PATTERN)];
600
+ for (const match of matchs) {
601
+ const [matched, key] = match;
602
+ if (!prefsWithoutPrefix[key] && !prefsWithoutPrefix[key]) {
603
+ this.logger.warn(`preference key '${key}' in ${path} not init in prefs.js`);
604
+ continue;
605
+ }
606
+ if (key.startsWith(prefix))
607
+ continue;
608
+ else
609
+ content = content.replace(matched, `preference="${prefix}.${key}"`);
610
+ }
611
+ await outputFile(path, content, "utf-8");
612
+ }));
613
+ }
614
+ }
475
615
  esbuild() {
476
616
  const { build: { esbuildOptions } } = this.ctx;
477
617
  if (esbuildOptions.length === 0)
@@ -503,7 +643,7 @@ class Build extends Base {
503
643
  },
504
644
  applications: {
505
645
  zotero: {
506
- strict_min_version: min,
646
+ ...min && { strict_min_version: min },
507
647
  ...max && { strict_max_version: max }
508
648
  }
509
649
  }
@@ -1176,18 +1316,43 @@ function findFreeTcpPort() {
1176
1316
  });
1177
1317
  }
1178
1318
 
1319
+ const default_options = {
1320
+ binary: {
1321
+ // path: "",
1322
+ args: [],
1323
+ devtools: true
1324
+ },
1325
+ profile: {
1326
+ path: "./.scaffold/profile",
1327
+ dataDir: "",
1328
+ // keepChanges: true,
1329
+ createIfMissing: true,
1330
+ customPrefs: {}
1331
+ },
1332
+ plugins: {
1333
+ asProxy: false,
1334
+ list: []
1335
+ }
1336
+ };
1179
1337
  class ZoteroRunner {
1180
1338
  options;
1181
- remoteFirefox;
1339
+ remoteFirefox = new RemoteFirefox();
1182
1340
  zotero;
1183
1341
  constructor(options) {
1184
- this.options = options;
1185
- this.remoteFirefox = new RemoteFirefox();
1342
+ this.options = toMerged(default_options, options);
1343
+ if (!options.binary.path)
1344
+ throw new Error("Binary path must be provided.");
1345
+ if (!options.profile.path && !options.profile.dataDir)
1346
+ this.options.profile.dataDir = "./.scaffold/data";
1347
+ logger.debug(this.options);
1348
+ }
1349
+ get default_profile_path() {
1350
+ return default_options.profile.path;
1186
1351
  }
1187
1352
  async run() {
1188
- await this.setupProfileDir();
1353
+ await this.setupProfile();
1189
1354
  await this.startZoteroInstance();
1190
- if (!this.options.asProxy)
1355
+ if (!this.options.plugins.asProxy)
1191
1356
  await this.installTemporaryPlugins();
1192
1357
  }
1193
1358
  /**
@@ -1197,45 +1362,51 @@ class ZoteroRunner {
1197
1362
  *
1198
1363
  * @see https://www.zotero.org/support/dev/client_coding/plugin_development#setting_up_a_plugin_development_environment
1199
1364
  */
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
- });
1365
+ async setupProfile() {
1366
+ const { path, createIfMissing } = this.options.profile;
1367
+ if (!await pathExists(path)) {
1368
+ if (createIfMissing)
1369
+ await this.createProfile(this.default_profile_path);
1370
+ else
1371
+ throw new Error("The 'profile.path' must be provided when 'createIfMissing' is false.");
1218
1372
  }
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) {
1373
+ const prefsPath = join(this.options.profile.path, "prefs.js");
1374
+ const prefsManager = new PrefsManager("user_pref");
1375
+ prefsManager.setPrefs(prefs);
1376
+ prefsManager.setPrefs(this.options.profile.customPrefs);
1377
+ if (await pathExists(prefsPath))
1378
+ await prefsManager.read(prefsPath);
1379
+ prefsManager.setPrefs({
1380
+ "extensions.lastAppBuildId": null,
1381
+ "extensions.lastAppVersion": null
1382
+ });
1383
+ await prefsManager.write(prefsPath);
1384
+ if (this.options.plugins.asProxy) {
1223
1385
  await this.installProxyPlugins();
1224
1386
  }
1225
1387
  }
1388
+ async createProfile(path) {
1389
+ logger.debug(`Creating profile at ${this.default_profile_path}...`);
1390
+ await ensureDir(path);
1391
+ }
1392
+ // private async copyProfile(from: string, to = this.default_profile_path) {
1393
+ // logger.debug(`Copying profile from '${this.options.profile.path}' to ${this.default_profile_path}...`);
1394
+ // await copy(from, to);
1395
+ // this.options.profile.path = to;
1396
+ // }
1226
1397
  async startZoteroInstance() {
1227
1398
  let args = ["--purgecaches", "no-remote"];
1228
- if (this.options.profilePath) {
1229
- args.push("-profile", resolve(this.options.profilePath));
1399
+ if (this.options.profile.path) {
1400
+ args.push("-profile", resolve(this.options.profile.path));
1230
1401
  }
1231
- if (this.options.dataDir) {
1232
- args.push("--dataDir", resolve(this.options.dataDir));
1402
+ if (this.options.profile.dataDir) {
1403
+ args.push("--dataDir", resolve(this.options.profile.dataDir));
1233
1404
  }
1234
- if (this.options.devtools) {
1405
+ if (this.options.binary.devtools) {
1235
1406
  args.push("--jsdebugger");
1236
1407
  }
1237
- if (this.options.binaryArgs) {
1238
- args = [...args, ...this.options.binaryArgs];
1408
+ if (this.options.binary.args) {
1409
+ args = [...args, ...this.options.binary.args];
1239
1410
  }
1240
1411
  const remotePort = await findFreeTcpPort();
1241
1412
  args.push("-start-debugger-server", String(remotePort));
@@ -1245,7 +1416,9 @@ class ZoteroRunner {
1245
1416
  XPCOM_DEBUG_BREAK: "stack",
1246
1417
  NS_TRACE_MALLOC_DISABLE_STACKS: "1"
1247
1418
  };
1248
- this.zotero = spawn(this.options.binaryPath, args, { env });
1419
+ if (!await pathExists(this.options.binary.path))
1420
+ throw new Error("The Zotero binary not found.");
1421
+ this.zotero = spawn(this.options.binary.path, args, { env });
1249
1422
  logger.debug("Zotero started, pid:", this.zotero.pid);
1250
1423
  this.zotero.stdout?.on("data", (_data) => {
1251
1424
  });
@@ -1254,7 +1427,7 @@ class ZoteroRunner {
1254
1427
  logger.debug(`Connected to the remote Firefox debugger on port: ${remotePort}`);
1255
1428
  }
1256
1429
  async installTemporaryPlugins() {
1257
- for (const plugin of this.options.plugins) {
1430
+ for (const plugin of this.options.plugins.list) {
1258
1431
  const addonId = await this.remoteFirefox.installTemporaryAddon(resolve(plugin.sourceDir)).then((installResult) => {
1259
1432
  return installResult.addon.id;
1260
1433
  });
@@ -1264,7 +1437,7 @@ class ZoteroRunner {
1264
1437
  }
1265
1438
  }
1266
1439
  async installProxyPlugin(id, sourceDir) {
1267
- const addonProxyFilePath = join(this.options.profilePath, `extensions/${id}`);
1440
+ const addonProxyFilePath = join(this.options.profile.path, `extensions/${id}`);
1268
1441
  const buildPath = resolve(sourceDir);
1269
1442
  await outputFile(addonProxyFilePath, buildPath);
1270
1443
  logger.debug(
@@ -1274,12 +1447,12 @@ class ZoteroRunner {
1274
1447
  ` Addon path: ${buildPath}`
1275
1448
  ].join("\n")
1276
1449
  );
1277
- const addonXpiFilePath = join(this.options.profilePath, `extensions/${id}.xpi`);
1450
+ const addonXpiFilePath = join(this.options.profile.path, `extensions/${id}.xpi`);
1278
1451
  if (await pathExists(addonXpiFilePath)) {
1279
1452
  await remove(addonXpiFilePath);
1280
1453
  logger.debug(`XPI file found, removed.`);
1281
1454
  }
1282
- const addonInfoFilePath = join(this.options.profilePath, "extensions.json");
1455
+ const addonInfoFilePath = join(this.options.profile.path, "extensions.json");
1283
1456
  if (await pathExists(addonInfoFilePath)) {
1284
1457
  const content = await readJSON(addonInfoFilePath);
1285
1458
  content.addons = content.addons.map((addon) => {
@@ -1294,7 +1467,7 @@ class ZoteroRunner {
1294
1467
  }
1295
1468
  }
1296
1469
  async installProxyPlugins() {
1297
- for (const { id, sourceDir } of this.options.plugins) {
1470
+ for (const { id, sourceDir } of this.options.plugins.list) {
1298
1471
  await this.installProxyPlugin(id, sourceDir);
1299
1472
  }
1300
1473
  }
@@ -1302,7 +1475,7 @@ class ZoteroRunner {
1302
1475
  await this.remoteFirefox.reloadAddon(id);
1303
1476
  }
1304
1477
  async reloadTemporaryPluginBySourceDir(sourceDir) {
1305
- const addonId = this.options.plugins.find((p) => p.sourceDir === sourceDir)?.id;
1478
+ const addonId = this.options.plugins.list.find((p) => p.sourceDir === sourceDir)?.id;
1306
1479
  if (!addonId) {
1307
1480
  return {
1308
1481
  sourceDir,
@@ -1322,7 +1495,7 @@ class ZoteroRunner {
1322
1495
  return { sourceDir, reloadError: void 0 };
1323
1496
  }
1324
1497
  async reloadAllTemporaryPlugins() {
1325
- for (const { sourceDir } of this.options.plugins) {
1498
+ for (const { sourceDir } of this.options.plugins.list) {
1326
1499
  const res = await this.reloadTemporaryPluginBySourceDir(sourceDir);
1327
1500
  if (res.reloadError instanceof Error) {
1328
1501
  logger.error(res.reloadError);
@@ -1349,20 +1522,20 @@ class ZoteroRunner {
1349
1522
  const url = `zotero://ztoolkit-debug/?run=${encodeURIComponent(
1350
1523
  reloadScript
1351
1524
  )}`;
1352
- const startZoteroCmd = `"${this.options.binaryPath}" --purgecaches -profile "${this.options.profilePath}"`;
1525
+ const startZoteroCmd = `"${this.options.binary.path}" --purgecaches -profile "${this.options.profile.path}"`;
1353
1526
  const command = `${startZoteroCmd} -url "${url}"`;
1354
1527
  execSync(command);
1355
1528
  }
1356
1529
  // Do not use this method if possible,
1357
1530
  // as frequent execSync can cause Zotero to crash.
1358
1531
  async reloadAllProxyPlugins() {
1359
- for (const { id } of this.options.plugins) {
1532
+ for (const { id } of this.options.plugins.list) {
1360
1533
  await this.reloadProxyPluginByZToolkit(id, id, id);
1361
1534
  await delay(2e3);
1362
1535
  }
1363
1536
  }
1364
1537
  async reloadAllPlugins() {
1365
- if (this.options.asProxy)
1538
+ if (this.options.plugins.asProxy)
1366
1539
  await this.reloadAllProxyPlugins();
1367
1540
  else
1368
1541
  await this.reloadAllTemporaryPlugins();
@@ -1401,7 +1574,6 @@ class Serve extends Base {
1401
1574
  builder;
1402
1575
  runner;
1403
1576
  _zoteroBinPath;
1404
- _profilePath;
1405
1577
  constructor(ctx) {
1406
1578
  super(ctx);
1407
1579
  process.env.NODE_ENV ??= "development";
@@ -1410,20 +1582,30 @@ class Serve extends Base {
1410
1582
  async run() {
1411
1583
  process.on("SIGINT", this.exit);
1412
1584
  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
1585
+ binary: {
1586
+ path: this.zoteroBinPath,
1587
+ devtools: this.ctx.server.devtools,
1588
+ args: this.ctx.server.startArgs
1589
+ },
1590
+ profile: {
1591
+ path: this.profilePath,
1592
+ dataDir: this.dataDir,
1593
+ // keepChanges: this.ctx.server.keepProfileChanges,
1594
+ createIfMissing: this.ctx.server.createProfileIfMissing
1595
+ },
1596
+ plugins: {
1597
+ list: [{
1598
+ id: this.ctx.id,
1599
+ sourceDir: join(this.ctx.dist, "addon")
1600
+ }],
1601
+ asProxy: this.ctx.server.asProxy
1602
+ }
1423
1603
  });
1424
1604
  await this.ctx.hooks.callHook("serve:init", this.ctx);
1425
- await this.builder.run();
1426
- await this.ctx.hooks.callHook("serve:prebuild", this.ctx);
1605
+ if (this.ctx.server.prebuild) {
1606
+ await this.builder.run();
1607
+ await this.ctx.hooks.callHook("serve:prebuild", this.ctx);
1608
+ }
1427
1609
  await this.runner.run();
1428
1610
  this.runner.zotero?.on("exit", this.onZoteroExit);
1429
1611
  this.runner.zotero?.on("close", this.onZoteroExit);
@@ -1490,15 +1672,10 @@ class Serve extends Base {
1490
1672
  return this._zoteroBinPath;
1491
1673
  }
1492
1674
  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;
1675
+ return process.env.ZOTERO_PLUGIN_PROFILE_PATH;
1499
1676
  }
1500
1677
  get dataDir() {
1501
- return process.env.ZOTERO_PLUGIN_DATA_DIR ?? "";
1678
+ return process.env.ZOTERO_PLUGIN_DATA_DIR;
1502
1679
  }
1503
1680
  }
1504
1681
 
@@ -2036,19 +2213,25 @@ class Test extends Base {
2036
2213
  }
2037
2214
  async startZotero() {
2038
2215
  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
2216
+ binary: {
2217
+ path: this.zoteroBinPath,
2218
+ devtools: this.ctx.server.devtools,
2219
+ args: this.ctx.server.startArgs
2220
+ },
2221
+ profile: {
2222
+ path: this.profilePath,
2223
+ dataDir: this.dataDir,
2224
+ customPrefs: this.prefs
2225
+ },
2226
+ plugins: {
2227
+ list: [{
2228
+ id: this.ctx.id,
2229
+ sourceDir: join(this.ctx.dist, "addon")
2230
+ }, {
2231
+ id: this.testPluginID,
2232
+ sourceDir: this.testPluginDir
2233
+ }]
2234
+ }
2052
2235
  });
2053
2236
  await this.runner.run();
2054
2237
  }
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.15",
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",