zotero-plugin-scaffold 0.4.2 → 0.5.0

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.
package/dist/cli.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import process from 'node:process';
2
2
  import { Command } from 'commander';
3
- import { l as logger, E as ExitSignals, C as Config, B as Build, S as Serve, T as Test, R as Release } from './shared/zotero-plugin-scaffold.DGHktPRh.mjs';
3
+ import { l as logger, E as ExitSignals, C as Config, B as Build, S as Serve, T as Test, R as Release } from './shared/zotero-plugin-scaffold.BhyPDymw.mjs';
4
4
  import { readFile } from 'node:fs/promises';
5
5
  import { pathExists } from 'fs-extra';
6
6
  import tinyUpdateNotifier from 'tiny-update-notifier';
@@ -13,11 +13,12 @@ import 'node:util';
13
13
  import 'std-env';
14
14
  import './shared/zotero-plugin-scaffold.v8dJZZ-v.mjs';
15
15
  import 'tinyglobby';
16
- import 'node:fs';
17
16
  import 'node:path';
18
17
  import 'adm-zip';
19
18
  import 'esbuild';
20
19
  import 'node:crypto';
20
+ import 'node:fs';
21
+ import '@fluent/syntax';
21
22
  import '@swc/core';
22
23
  import 'node:child_process';
23
24
  import 'bumpp';
@@ -31,7 +32,7 @@ import 'xvfb-ts';
31
32
  import 'node:http';
32
33
 
33
34
  const name = "zotero-plugin-scaffold";
34
- const version = "0.4.2";
35
+ const version = "0.5.0";
35
36
  const pkg = {
36
37
  name: name,
37
38
  version: version};
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { B as Build, C as Config, R as Release, S as Serve, T as Test, d as defineConfig } from './shared/zotero-plugin-scaffold.DGHktPRh.mjs';
1
+ export { B as Build, C as Config, R as Release, S as Serve, T as Test, d as defineConfig } from './shared/zotero-plugin-scaffold.BhyPDymw.mjs';
2
2
  import 'c12';
3
3
  import 'es-toolkit';
4
4
  import 'fs-extra/esm';
@@ -10,11 +10,12 @@ 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';
14
13
  import 'node:path';
15
14
  import 'adm-zip';
16
15
  import 'esbuild';
17
16
  import 'node:crypto';
17
+ import 'node:fs';
18
+ import '@fluent/syntax';
18
19
  import '@swc/core';
19
20
  import 'node:child_process';
20
21
  import 'bumpp';
@@ -1,19 +1,20 @@
1
1
  import { loadConfig as loadConfig$1 } from 'c12';
2
2
  import { isPlainObject, kebabCase, mapValues, toMerged, escapeRegExp, debounce, delay } from 'es-toolkit';
3
- import { readJsonSync, outputFile, emptyDir, copy, readJSON, outputJSON, move, writeJson, pathExists, ensureDir, remove } from 'fs-extra/esm';
3
+ import { readJsonSync, move, outputFile, emptyDir, copy, readJSON, outputJSON, 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 util from 'node:util';
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';
11
- import { readFile, writeFile, stat } from 'node:fs/promises';
12
- import { basename, dirname, join, resolve, extname } from 'node:path';
10
+ import { dirname, basename, join, resolve, extname } from 'node:path';
13
11
  import AdmZip from 'adm-zip';
14
12
  import { build, context } from 'esbuild';
15
13
  import { glob, globSync } from 'tinyglobby';
16
14
  import { createHash } from 'node:crypto';
15
+ import { createReadStream, existsSync } from 'node:fs';
16
+ import { readFile, writeFile, stat } from 'node:fs/promises';
17
+ import { parse, serialize, Transformer } from '@fluent/syntax';
17
18
  import { parseSync, printSync } from '@swc/core';
18
19
  import { execSync, spawn } from 'node:child_process';
19
20
  import { versionBump, ProgressEvent } from 'bumpp';
@@ -359,6 +360,192 @@ function generateHash(filePath, algorithm) {
359
360
  });
360
361
  }
361
362
 
363
+ class Base {
364
+ ctx;
365
+ constructor(ctx) {
366
+ this.ctx = ctx;
367
+ }
368
+ get logger() {
369
+ return this.ctx.logger;
370
+ }
371
+ }
372
+
373
+ async function buildLocale(dist, namespace, options) {
374
+ const ignores = toArray(options.ignore);
375
+ const localeNames = await getLocales(dist);
376
+ const messageManager = new MessageManager(ignores);
377
+ await Promise.all(localeNames.map(async (locale) => {
378
+ const paths = await glob(`${dist}/addon/locale/${locale}/**/*.ftl`);
379
+ await Promise.all(paths.map(async (path) => {
380
+ const fm = new FluentManager();
381
+ await fm.read(path);
382
+ messageManager.addMessages(locale, fm.getMessages());
383
+ if (options.prefixFluentMessages) {
384
+ fm.prefixMessages(namespace);
385
+ await fm.write(path);
386
+ }
387
+ if (options.prefixLocaleFiles) {
388
+ const newPath = `${dirname(path)}/${namespace}-${basename(path)}`;
389
+ await move(path, newPath);
390
+ logger.debug(`Renamed FTL: ${path} \u2192 ${newPath}`);
391
+ }
392
+ }));
393
+ }));
394
+ const htmlPaths = await glob([`${dist}/addon/**/*.xhtml`, `${dist}/addon/**/*.html`]);
395
+ await Promise.all(htmlPaths.map(async (htmlPath) => {
396
+ const content = await readFile(htmlPath, "utf-8");
397
+ const { processedContent, foundMessages } = processHTMLFile(
398
+ content,
399
+ namespace,
400
+ messageManager.getFTLMessages(),
401
+ ignores,
402
+ htmlPath
403
+ );
404
+ messageManager.addMessages("html", foundMessages);
405
+ if (options.prefixFluentMessages) {
406
+ await writeFile(htmlPath, processedContent);
407
+ }
408
+ }));
409
+ messageManager.validateMessages();
410
+ }
411
+ async function getLocales(dist) {
412
+ const localePaths = await glob(`${dist}/addon/locale/*`, { onlyDirectories: true });
413
+ return localePaths.map((p) => basename(p));
414
+ }
415
+ class FluentManager {
416
+ source;
417
+ resource;
418
+ messages = [];
419
+ constructor() {
420
+ }
421
+ // Parse Fluent source into an AST and extract messages
422
+ parse(source) {
423
+ this.source = source;
424
+ this.resource = parse(source, {});
425
+ }
426
+ // Read a file, parse its content, and extract messages
427
+ async read(path) {
428
+ const content = await readFile(path, "utf-8");
429
+ this.parse(content);
430
+ }
431
+ // Extract message IDs from the parsed resource
432
+ getMessages() {
433
+ if (!this.resource) {
434
+ throw new Error("Resource must be parsed first.");
435
+ }
436
+ this.messages.length = 0;
437
+ this.messages.push(
438
+ ...this.resource.body.filter((entry) => entry.type === "Message").map((message) => message.id.name)
439
+ );
440
+ return this.messages;
441
+ }
442
+ // Apply namespace prefix to message IDs in the resource
443
+ prefixMessages(namespace) {
444
+ if (!this.resource) {
445
+ throw new Error("Resource must be parsed before applying prefix.");
446
+ }
447
+ new FluentTransformer(namespace).genericVisit(this.resource);
448
+ }
449
+ // Serialize the resource back into a string
450
+ serialize() {
451
+ if (!this.resource) {
452
+ throw new Error("Resource not parsed. Cannot serialize.");
453
+ }
454
+ return serialize(this.resource, {});
455
+ }
456
+ // Write the serialized resource to a file
457
+ async write(path) {
458
+ const result = this.serialize();
459
+ if (result !== this.source)
460
+ await writeFile(path, this.serialize());
461
+ }
462
+ }
463
+ class FluentTransformer extends Transformer {
464
+ constructor(prefix) {
465
+ super();
466
+ this.prefix = prefix;
467
+ }
468
+ needsPrefix(name) {
469
+ return !!this.prefix && !name.startsWith(this.prefix);
470
+ }
471
+ visitMessage(node) {
472
+ if (this.needsPrefix(node.id.name)) {
473
+ node.id.name = `${this.prefix}-${node.id.name}`;
474
+ }
475
+ return this.genericVisit(node);
476
+ }
477
+ visitMessageReference(node) {
478
+ if (this.needsPrefix(node.id.name)) {
479
+ node.id.name = `${this.prefix}-${node.id.name}`;
480
+ }
481
+ return this.genericVisit(node);
482
+ }
483
+ }
484
+ class MessageManager {
485
+ ftlMessages = /* @__PURE__ */ new Map();
486
+ htmlMessages = /* @__PURE__ */ new Set();
487
+ ignores;
488
+ constructor(ignores) {
489
+ this.ignores = ignores;
490
+ }
491
+ // Add a set of messages (FTL or HTML) for a specific locale or for HTML globally
492
+ addMessages(target, messages) {
493
+ if (target === "html") {
494
+ messages.forEach((msg) => this.htmlMessages.add(msg));
495
+ } else {
496
+ let ftlLocaleMessages = this.ftlMessages.get(target);
497
+ if (!ftlLocaleMessages) {
498
+ ftlLocaleMessages = /* @__PURE__ */ new Set();
499
+ this.ftlMessages.set(target, ftlLocaleMessages);
500
+ }
501
+ messages.forEach((msg) => ftlLocaleMessages.add(msg));
502
+ }
503
+ }
504
+ validateMessages() {
505
+ this.htmlMessages.forEach((msg) => {
506
+ if (this.ignores.includes(msg))
507
+ return;
508
+ const missingLocales = [...this.ftlMessages.entries()].filter(([_, messages]) => !messages.has(msg)).map(([locale]) => locale);
509
+ if (missingLocales.length > 0)
510
+ logger.warn(`I10N id ${styleText.blue(msg)} missing in locale: ${missingLocales.join(", ")}`);
511
+ });
512
+ }
513
+ getFTLMessages() {
514
+ const allMessages = /* @__PURE__ */ new Set();
515
+ this.ftlMessages.forEach((messages) => messages.forEach((msg) => allMessages.add(msg)));
516
+ return allMessages;
517
+ }
518
+ // Get all FTL messages for a specific locale
519
+ getFTLMessagesByLocale(locale) {
520
+ return this.ftlMessages.get(locale) || /* @__PURE__ */ new Set();
521
+ }
522
+ // Get all HTML messages
523
+ getHTMLMessages() {
524
+ return this.htmlMessages;
525
+ }
526
+ }
527
+ function processHTMLFile(content, namespace, allMessages, ignores, filePath) {
528
+ const foundMessages = /* @__PURE__ */ new Set();
529
+ const L10N_PATTERN = new RegExp(`(data-l10n-id)="((?!${namespace})\\S*)"`, "g");
530
+ const processed = content.replace(L10N_PATTERN, (match, attr, id) => {
531
+ foundMessages.add(id);
532
+ if (ignores.includes(id)) {
533
+ logger.debug(`Skipped ignored ID: ${styleText.blue(id)} in ${styleText.gray(filePath)}`);
534
+ return match;
535
+ }
536
+ if (!allMessages.has(id)) {
537
+ logger.warn(`I10N id ${styleText.blue(id)} in path ${styleText.gray(filePath)} does not exist in any locale, skip renaming it.`);
538
+ return match;
539
+ }
540
+ return `${attr}="${namespace}-${id}"`;
541
+ });
542
+ return { processedContent: processed, foundMessages: [...foundMessages] };
543
+ }
544
+
545
+ function is32BitNumber(n) {
546
+ return Number.isInteger(n) && n >= -2147483648 && n <= 2147483647;
547
+ }
548
+
362
549
  class PrefsManager {
363
550
  namespace;
364
551
  prefs = {};
@@ -580,6 +767,62 @@ class PrefsManager {
580
767
  return _prefs;
581
768
  }
582
769
  }
770
+
771
+ async function buildPrefs(dist, options) {
772
+ const { dts, prefixPrefKeys, prefix } = options;
773
+ if (!prefixPrefKeys && !dts)
774
+ return;
775
+ const prefsFilePath = join(dist, "addon", "prefs.js");
776
+ if (!existsSync(prefsFilePath))
777
+ return;
778
+ const prefsManager = new PrefsManager("pref");
779
+ await prefsManager.read(prefsFilePath);
780
+ const prefsWithPrefix = prefsManager.getPrefsWithPrefix(prefix);
781
+ const prefsWithoutPrefix = prefsManager.getPrefsWithoutPrefix(prefix);
782
+ const prefs = prefsManager.getPrefs();
783
+ Object.entries(prefs).forEach(([key, value]) => {
784
+ if (typeof value === "number") {
785
+ if (!is32BitNumber(value)) {
786
+ logger.warn(`Pref key '${styleText.blue(key)}' is a number, but is more than 4 bytes, which can be problematic on some OS.`);
787
+ }
788
+ }
789
+ });
790
+ if (dts) {
791
+ const dtsContent = renderPluginPrefsDts(prefsWithoutPrefix);
792
+ await outputFile(dts, dtsContent, "utf-8");
793
+ }
794
+ if (prefixPrefKeys) {
795
+ prefsManager.clearPrefs();
796
+ prefsManager.setPrefs(prefsWithPrefix);
797
+ await prefsManager.write(prefsFilePath);
798
+ }
799
+ if (prefixPrefKeys) {
800
+ const HTML_PREFERENCE_PATTERN = /preference="(\S*)"/g;
801
+ const xhtmlPaths = await glob(`${dist}/addon/**/*.xhtml`);
802
+ await Promise.all(xhtmlPaths.map(async (path) => {
803
+ let content = await readFile(path, "utf-8");
804
+ const matchs = [...content.matchAll(HTML_PREFERENCE_PATTERN)];
805
+ for (const match of matchs) {
806
+ const [matched, key] = match;
807
+ if (key.startsWith(prefix)) {
808
+ logger.debug(`Pref key '${styleText.blue(key)}' is already starts with '${prefix}', skip prefixing it.`);
809
+ continue;
810
+ } else if (key.startsWith("extensions.")) {
811
+ logger.warn(`Pref key '${styleText.blue(key)}' in ${styleText.gray(path)} starts with 'extensions.' but not '${styleText.blue(prefix)}', skip prefixing it.`);
812
+ continue;
813
+ } else if (!(key in prefsWithPrefix) && !(key in prefsWithoutPrefix)) {
814
+ logger.warn(`Pref key '${styleText.blue(key)}' in ${styleText.gray(path)} is not found in prefs.js, skip prefixing it.`);
815
+ continue;
816
+ } else {
817
+ const prefixed = `${prefix}.${key}`;
818
+ logger.debug(`Pref key '${styleText.blue(key)}' in ${styleText.gray(path)} is prefixed to ${styleText.blue(prefixed)}.`);
819
+ content = content.replace(matched, `preference="${prefixed}"`);
820
+ }
821
+ }
822
+ await outputFile(path, content, "utf-8");
823
+ }));
824
+ }
825
+ }
583
826
  function renderPluginPrefsDts(prefs) {
584
827
  return `// Generated by zotero-plugin-scaffold
585
828
  /* prettier-ignore */
@@ -599,16 +842,6 @@ declare namespace _ZoteroTypes {
599
842
  `;
600
843
  }
601
844
 
602
- class Base {
603
- ctx;
604
- constructor(ctx) {
605
- this.ctx = ctx;
606
- }
607
- get logger() {
608
- return this.ctx.logger;
609
- }
610
- }
611
-
612
845
  class Build extends Base {
613
846
  buildTime;
614
847
  isPreRelease;
@@ -711,119 +944,11 @@ class Build extends Base {
711
944
  }
712
945
  async prepareLocaleFiles() {
713
946
  const { dist, namespace, build } = this.ctx;
714
- const ignores = toArray(build.fluent.ignore);
715
- const FTL_MESSAGE_PATTERN = /^(?<message>[a-z]\S*)( *= *)(?<pattern>.*)$/gim;
716
- const HTML_DATAI10NID_PATTERN = new RegExp(`(data-l10n-id)="((?!${namespace})\\S*)"`, "g");
717
- const localePaths = await glob(`${dist}/addon/locale/*`, { onlyDirectories: true });
718
- const localeNames = localePaths.map((locale) => basename(locale));
719
- this.logger.debug(`Locale names:", ${localeNames}`);
720
- const allMessages = /* @__PURE__ */ new Set();
721
- const messagesByLocale = /* @__PURE__ */ new Map();
722
- for (const localeName of localeNames) {
723
- const localeMessages = /* @__PURE__ */ new Set();
724
- const ftlPaths = await glob(`${dist}/addon/locale/${localeName}/**/*.ftl`);
725
- await Promise.all(ftlPaths.map(async (ftlPath) => {
726
- let ftlContent = await readFile(ftlPath, "utf-8");
727
- const matches = [...ftlContent.matchAll(FTL_MESSAGE_PATTERN)];
728
- for (const match of matches) {
729
- const [matched, message, _pattern] = match;
730
- if (message) {
731
- localeMessages.add(message);
732
- allMessages.add(message);
733
- ftlContent = ftlContent.replace(new RegExp(`^${escapeRegExp(matched)}`, "gm"), `${namespace}-${matched}`);
734
- }
735
- }
736
- if (build.fluent.prefixFluentMessages)
737
- await writeFile(ftlPath, ftlContent);
738
- if (build.fluent.prefixLocaleFiles === true) {
739
- await move(ftlPath, `${dirname(ftlPath)}/${namespace}-${basename(ftlPath)}`);
740
- this.logger.debug(`FTL file '${ftlPath}' is renamed to '${namespace}-${basename(ftlPath)}'.`);
741
- }
742
- }));
743
- messagesByLocale.set(localeName, localeMessages);
744
- }
745
- const messagesInHTML = /* @__PURE__ */ new Set();
746
- const htmlPaths = await glob([
747
- `${dist}/addon/**/*.xhtml`,
748
- `${dist}/addon/**/*.html`
749
- ]);
750
- await Promise.all(htmlPaths.map(async (htmlPath) => {
751
- let htmlContent = await readFile(htmlPath, "utf-8");
752
- const matches = [...htmlContent.matchAll(HTML_DATAI10NID_PATTERN)];
753
- for (const match of matches) {
754
- const [matched, attrKey, attrVal] = match;
755
- if (ignores.includes(attrVal)) {
756
- this.logger.debug(`HTML data-i10n-id ${attrVal} is in ignore list, skip to namespace`);
757
- continue;
758
- }
759
- if (!allMessages.has(attrVal)) {
760
- this.logger.warn(`HTML data-i10n-id '${styleText.blue(attrVal)}' in ${styleText.gray(htmlPath)} do not exist in any FTL message, skip to namespace it.`);
761
- continue;
762
- }
763
- messagesInHTML.add(attrVal);
764
- const namespacedAttr = `${namespace}-${attrVal}`;
765
- htmlContent = htmlContent.replace(matched, `${attrKey}="${namespacedAttr}"`);
766
- this.logger.debug(`HTML data-i10n-id '${styleText.blue(attrVal)}' in ${styleText.gray(htmlPath)} is namespaced to ${styleText.blue(namespacedAttr)}.`);
767
- }
768
- if (build.fluent.prefixFluentMessages)
769
- await writeFile(htmlPath, htmlContent);
770
- }));
771
- messagesInHTML.forEach((messageInHTML) => {
772
- if (ignores.includes(messageInHTML))
773
- return;
774
- const missingLocales = [...messagesByLocale.entries()].filter(([_, messages]) => !messages.has(messageInHTML)).map(([locale]) => locale);
775
- if (missingLocales.length > 0) {
776
- this.logger.warn(`HTML data-l10n-id '${styleText.blue(messageInHTML)}' is missing in locales: ${missingLocales.join(", ")}.`);
777
- }
778
- });
947
+ await buildLocale(dist, namespace, build.fluent);
779
948
  }
780
949
  async preparePrefs() {
781
- const { dts, prefixPrefKeys, prefix } = this.ctx.build.prefs;
782
950
  const { dist } = this.ctx;
783
- if (!prefixPrefKeys && !dts)
784
- return;
785
- const prefsFilePath = join(dist, "addon", "prefs.js");
786
- if (!existsSync(prefsFilePath))
787
- return;
788
- const prefsManager = new PrefsManager("pref");
789
- await prefsManager.read(prefsFilePath);
790
- const prefsWithPrefix = prefsManager.getPrefsWithPrefix(prefix);
791
- const prefsWithoutPrefix = prefsManager.getPrefsWithoutPrefix(prefix);
792
- if (dts) {
793
- const dtsContent = renderPluginPrefsDts(prefsWithoutPrefix);
794
- await outputFile(dts, dtsContent, "utf-8");
795
- }
796
- if (prefixPrefKeys) {
797
- prefsManager.clearPrefs();
798
- prefsManager.setPrefs(prefsWithPrefix);
799
- await prefsManager.write(prefsFilePath);
800
- }
801
- if (prefixPrefKeys) {
802
- const HTML_PREFERENCE_PATTERN = /preference="(\S*)"/g;
803
- const xhtmlPaths = await glob(`${dist}/addon/**/*.xhtml`);
804
- await Promise.all(xhtmlPaths.map(async (path) => {
805
- let content = await readFile(path, "utf-8");
806
- const matchs = [...content.matchAll(HTML_PREFERENCE_PATTERN)];
807
- for (const match of matchs) {
808
- const [matched, key] = match;
809
- if (key.startsWith(prefix)) {
810
- this.logger.debug(`Pref key '${styleText.blue(key)}' is already starts with '${prefix}', skip prefixing it.`);
811
- continue;
812
- } else if (key.startsWith("extensions.")) {
813
- this.logger.warn(`Pref key '${styleText.blue(key)}' in ${styleText.gray(path)} starts with 'extensions.' but not '${styleText.blue(prefix)}', skip prefixing it.`);
814
- continue;
815
- } else if (!(key in prefsWithPrefix) && !(key in prefsWithoutPrefix)) {
816
- this.logger.warn(`Pref key '${styleText.blue(key)}' in ${styleText.gray(path)} is not found in prefs.js, skip prefixing it.`);
817
- continue;
818
- } else {
819
- const prefixed = `${prefix}.${key}`;
820
- this.logger.debug(`Pref key '${styleText.blue(key)}' in ${styleText.gray(path)} is prefixed to ${styleText.blue(prefixed)}.`);
821
- content = content.replace(matched, `preference="${prefixed}"`);
822
- }
823
- }
824
- await outputFile(path, content, "utf-8");
825
- }));
826
- }
951
+ await buildPrefs(dist, this.ctx.build.prefs);
827
952
  }
828
953
  esbuild() {
829
954
  const { dist, build: { esbuildOptions } } = this.ctx;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "zotero-plugin-scaffold",
3
3
  "type": "module",
4
- "version": "0.4.2",
4
+ "version": "0.5.0",
5
5
  "description": "A scaffold for Zotero plugin development.",
6
6
  "author": "northword",
7
7
  "license": "AGPL-3.0-or-later",
@@ -63,6 +63,7 @@
63
63
  }
64
64
  },
65
65
  "dependencies": {
66
+ "@fluent/syntax": "^0.19.0",
66
67
  "@swc/core": "^1.11.16",
67
68
  "adm-zip": "^0.5.16",
68
69
  "bumpp": "^10.1.0",