Skip to content

Commit e178a3d

Browse files
authored
feat: parse prefs.js (#65)
* feat: parse prefs.js * fix: compatible with zotero-types * refactor: split func * refactor: use PrefsManager * tweaks * style: fix lint * docs: add docs for prefs manager
1 parent fae5548 commit e178a3d

File tree

6 files changed

+246
-20
lines changed

6 files changed

+246
-20
lines changed

docs/src/build.md

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -180,10 +180,85 @@ export default defineConfig({
180180
181181
### Preference Management
182182

183-
> In development.
183+
- Supports prefixing preference keys in `prefs.js`.
184+
- Generates TypeScript declaration files (`.d.ts`) for preferences.
185+
186+
Configure via `build.prefs`:
187+
188+
```ts twoslash
189+
import { defineConfig } from "zotero-plugin-scaffold";
190+
// ---cut---
191+
export default defineConfig({
192+
build: {
193+
prefs: {
194+
prefixPrefKeys: true,
195+
prefix: "extensions.myPlugin",
196+
dts: "typings/prefs.d.ts"
197+
}
198+
}
199+
});
200+
```
201+
202+
#### Adding Prefixes
203+
204+
When `build.prefs.prefixPrefKeys` is enabled, preferences should be written as follows:
205+
206+
::: code-group
207+
208+
```js [src/prefs.js]
209+
pref("lintOnAdded", true);
210+
```
211+
212+
```html [src/preference.xhtml]
213+
<vbox>
214+
<groupbox>
215+
<checkbox preference="lintOnAdded" data-l10n-id="linter-lint-on-item-added" native="true" />
216+
</groupbox>
217+
</vbox>
218+
```
219+
220+
```js [dist/prefs.js]
221+
pref("extensions.myPlugin.lintOnAdded", true);
222+
```
223+
224+
```html [dist/preference.xhtml]
225+
<vbox>
226+
<groupbox>
227+
<checkbox preference="extensions.myPlugin.lintOnAdded" data-l10n-id="linter-lint-on-item-added" native="true" />
228+
</groupbox>
229+
</vbox>
230+
```
184231

185-
- Supports prefixing preference keys in `prefs.js` with a custom namespace.
186-
- Optionally generates TypeScript declaration files (`.d.ts`) for preferences.
232+
:::
233+
234+
#### Generating DTS Files
235+
236+
Relies on the `zotero-types` package to provide type declarations for `Zotero.Prefs`.
237+
238+
You can also use the following helper to get or set preferences while omitting the prefix, simplifying your code.
239+
240+
```ts
241+
const PREFS_PREFIX = "extensions.myPlugin";
242+
243+
/**
244+
* Get preference value.
245+
* Wrapper of `Zotero.Prefs.get`.
246+
* @param key
247+
*/
248+
export function getPref<K extends keyof _PluginPrefsMap>(key: K) {
249+
return Zotero.Prefs.get(`${PREFS_PREFIX}.${key}` as PluginPrefKey<K>, true);
250+
}
251+
252+
/**
253+
* Set preference value.
254+
* Wrapper of `Zotero.Prefs.set`.
255+
* @param key
256+
* @param value
257+
*/
258+
export function setPref<K extends keyof _PluginPrefsMap>(key: K, value: PluginPrefsMap[PluginPrefKey<K>]) {
259+
return Zotero.Prefs.set(`${PREFS_PREFIX}.${key}` as PluginPrefKey<K>, value, true);
260+
}
261+
```
187262

188263
### Script Bundling
189264

packages/scaffold/src/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ function resolveConfig(config: Config): Context {
4545
config.id ||= config.name;
4646
config.namespace ||= config.name;
4747
config.xpiName ||= kebabCase(config.name);
48+
config.build.prefs.prefix ||= `extensions.${config.namespace}`;
4849

4950
// Parse template strings in config
5051
const isPreRelease = version.includes("-");
@@ -99,6 +100,11 @@ const defaultConfig = {
99100
prefixFluentMessages: true,
100101
prefixLocaleFiles: true,
101102
},
103+
prefs: {
104+
prefix: "",
105+
prefixPrefKeys: true,
106+
dts: "typings/prefs.d.ts",
107+
},
102108
esbuildOptions: [],
103109
makeManifest: {
104110
enable: true,

packages/scaffold/src/core/builder.ts

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import type { Context } from "../types/index.js";
22
import type { Manifest } from "../types/manifest.js";
33
import type { UpdateJSON } from "../types/update-json.js";
4+
import { existsSync } from "node:fs";
45
import { readFile, writeFile } from "node:fs/promises";
5-
import { basename, dirname } from "node:path";
6+
import { basename, dirname, join } from "node:path";
67
import process from "node:process";
78
import AdmZip from "adm-zip";
89
import chalk from "chalk";
910
import { toMerged } from "es-toolkit";
1011
import { build as buildAsync } from "esbuild";
11-
import { copy, emptyDir, move, outputJSON, readJSON, writeJson } from "fs-extra/esm";
12+
import { copy, emptyDir, move, outputFile, outputJSON, readJSON, writeJson } from "fs-extra/esm";
1213
import { glob } from "tinyglobby";
1314
import { generateHash } from "../utils/crypto.js";
15+
import { PrefsManager, renderPluginPrefsDts } from "../utils/prefs-manager.js";
1416
import { dateFormat, replaceInFile, toArray } from "../utils/string.js";
1517
import { Base } from "./base.js";
1618

@@ -52,6 +54,8 @@ export default class Build extends Base {
5254
await this.prepareLocaleFiles();
5355
await this.ctx.hooks.callHook("build:fluent", this.ctx);
5456

57+
await this.preparePrefs();
58+
5559
this.logger.tip("Bundling scripts");
5660
await this.esbuild();
5761
await this.ctx.hooks.callHook("build:bundle", this.ctx);
@@ -236,6 +240,61 @@ export default class Build extends Base {
236240
});
237241
}
238242

243+
async preparePrefs() {
244+
const { dts, prefixPrefKeys, prefix } = this.ctx.build.prefs;
245+
const { dist } = this.ctx;
246+
247+
// Skip if not enable this builder
248+
if (!prefixPrefKeys && !dts)
249+
return;
250+
251+
// Skip if no prefs.js
252+
const prefsFilePath = join(dist, "addon", "prefs.js");
253+
if (!existsSync(prefsFilePath))
254+
return;
255+
256+
// Parse prefs.js
257+
const prefsManager = new PrefsManager("pref");
258+
await prefsManager.read(prefsFilePath);
259+
const prefsWithPrefix = prefsManager.getPrefsWithPrefix(prefix);
260+
const prefsWithoutPrefix = prefsManager.getPrefsWithoutPrefix(prefix);
261+
262+
// Generate prefs.d.ts
263+
if (dts) {
264+
const dtsContent = renderPluginPrefsDts(prefsWithoutPrefix, prefix);
265+
await outputFile(dts, dtsContent, "utf-8");
266+
}
267+
268+
// Generate prefixed prefs.js
269+
if (prefixPrefKeys) {
270+
prefsManager.clearPrefs();
271+
prefsManager.setPrefs(prefsWithPrefix);
272+
await prefsManager.write(prefsFilePath);
273+
}
274+
275+
// Prefix pref keys in xhtml
276+
if (prefixPrefKeys) {
277+
const HTML_PREFERENCE_PATTERN = new RegExp(`preference="((?!${prefix})\\S*)"`, "g");
278+
const xhtmlPaths = await glob(`${dist}/addon/**/*.xhtml`);
279+
await Promise.all(xhtmlPaths.map(async (path) => {
280+
let content = await readFile(path, "utf-8");
281+
const matchs = [...content.matchAll(HTML_PREFERENCE_PATTERN)];
282+
for (const match of matchs) {
283+
const [matched, key] = match;
284+
if (!prefsWithoutPrefix[key] && !prefsWithoutPrefix[key]) {
285+
this.logger.warn(`preference key '${key}' in ${path} not init in prefs.js`);
286+
continue;
287+
}
288+
if (key.startsWith(prefix))
289+
continue;
290+
else
291+
content = content.replace(matched, `preference="${prefix}.${key}"`);
292+
}
293+
await outputFile(path, content, "utf-8");
294+
}));
295+
}
296+
}
297+
239298
esbuild() {
240299
const { build: { esbuildOptions } } = this.ctx;
241300

packages/scaffold/src/types/config.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,40 @@ export interface BuildConfig {
220220
*/
221221
prefixFluentMessages: boolean;
222222
};
223+
prefs: {
224+
/**
225+
* Prefixes the keys of preferences in `prefs.js` and
226+
* the value of the `preference` attribute in XHTML files.
227+
*
228+
* 为 `prefs.js` 中的首选项键和 XHTML 文件中的 `preference` 属性的值
229+
* 添加前缀。
230+
*
231+
* @default true
232+
*/
233+
prefixPrefKeys: boolean;
234+
/**
235+
* Prefixes for Preference Keys, no dot at the end.
236+
*
237+
* 首选项键的前缀,结尾不需要加点号。
238+
*
239+
* @default 'extensions.${namespace}'
240+
*/
241+
prefix: string;
242+
/**
243+
* Generate prefs.d.ts form prefs.js.
244+
*
245+
* - false: disable
246+
* - string: path of dts file
247+
*
248+
* 为首选项生成类型声明文件。
249+
*
250+
* - false: 禁用
251+
* - string:dts 文件路径
252+
*
253+
* @default 'typings/prefs.d.ts'
254+
*/
255+
dts: false | string;
256+
};
223257
/**
224258
* Configurations of esbuild.
225259
*

packages/scaffold/src/utils/prefs.ts renamed to packages/scaffold/src/utils/prefs-manager.ts

Lines changed: 63 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,18 @@ import { readFile } from "node:fs/promises";
22
import { isNotNil } from "es-toolkit";
33
import { outputFile } from "fs-extra/esm";
44
import { logger } from "./log.js";
5-
import { prefs as defaultPrefs } from "./zotero/preference.js";
65

76
export type Prefs = Record<string, string | number | boolean | undefined | null>;
87

98
export class PrefsManager {
109
private namespace: "pref" | "user_pref";
11-
private prefs: Prefs;
10+
private prefs: Prefs = {};
1211

1312
constructor(namespace: "pref" | "user_pref") {
1413
this.namespace = namespace;
15-
this.prefs = { ...defaultPrefs };
1614
}
1715

18-
private parsePrefjs(content: string) {
16+
private parse(content: string) {
1917
// eslint-disable-next-line regexp/no-super-linear-backtracking
2018
const prefPattern = /^(pref|user_pref)\s*\(\s*["']([^"']+)["']\s*,\s*(.+)\s*\)\s*;$/gm;
2119
const matches = content.matchAll(prefPattern);
@@ -26,12 +24,7 @@ export class PrefsManager {
2624
};
2725
}
2826

29-
public async read(path: string) {
30-
const content = await readFile(path, "utf-8");
31-
this.parsePrefjs(content);
32-
}
33-
34-
private renderPrefjs() {
27+
private render() {
3528
return Object.entries(this.prefs).map(([key, value]) => {
3629
if (!isNotNil(value))
3730
return "";
@@ -41,7 +34,7 @@ export class PrefsManager {
4134
cleanValue = `${value}`;
4235
}
4336
else if (typeof value === "string") {
44-
cleanValue = `${value.replace("\n", "\\n")}`;
37+
cleanValue = value; // `${value.replace("\n", "\\n")}`;
4538
}
4639
else if (typeof value === "number") {
4740
cleanValue = value.toString();
@@ -51,8 +44,13 @@ export class PrefsManager {
5144
}).filter(c => !!c).join("\n");
5245
}
5346

54-
public async write(path: string) {
55-
const content = this.renderPrefjs();
47+
async read(path: string) {
48+
const content = await readFile(path, "utf-8");
49+
this.parse(content);
50+
}
51+
52+
async write(path: string) {
53+
const content = this.render();
5654
// console.log(content);
5755
await outputFile(path, content, "utf-8");
5856
logger.debug("The <profile>/prefs.js has been modified.");
@@ -75,4 +73,56 @@ export class PrefsManager {
7573
getPrefs() {
7674
return this.prefs;
7775
}
76+
77+
clearPrefs() {
78+
this.prefs = {};
79+
}
80+
81+
getPrefsWithPrefix(prefix: string) {
82+
const _prefs: Prefs = {};
83+
for (const pref in this.prefs) {
84+
if (pref.startsWith(prefix))
85+
_prefs[pref] = this.prefs[pref];
86+
else
87+
_prefs[`${prefix}.${pref}`] = this.prefs[pref];
88+
}
89+
return _prefs;
90+
}
91+
92+
getPrefsWithoutPrefix(prefix: string) {
93+
const _prefs: Prefs = {};
94+
for (const pref in this.prefs) {
95+
_prefs[pref.replace(`${prefix}.`, "")] = this.prefs[pref];
96+
}
97+
return _prefs;
98+
}
99+
}
100+
101+
export function renderPluginPrefsDts(prefs: Prefs, prefix: string) {
102+
const dtsContent = `
103+
/* prettier-ignore */
104+
/* eslint-disable */
105+
// @ts-nocheck
106+
// Generated by zotero-plugin-scaffold
107+
108+
type _PluginPrefsMap = {
109+
${Object.entries(prefs).map(([key, value]) => {
110+
return `"${key}": ${typeof value};`;
111+
}).join("\n ")}
112+
};
113+
114+
type PluginPrefKey<K extends keyof _PluginPrefsMap> = \`${prefix}.\${K}\`;
115+
116+
type PluginPrefsMap = {
117+
[K in keyof _PluginPrefsMap as PluginPrefKey<K>]: _PluginPrefsMap[K]
118+
};
119+
120+
declare namespace _ZoteroTypes {
121+
interface Prefs {
122+
get: <K extends keyof PluginPrefsMap>(key: K, global?: boolean) => PluginPrefsMap[K];
123+
set: <K extends keyof PluginPrefsMap>(key: K, value: PluginPrefsMap[K], global?: boolean) => any;
124+
}
125+
}
126+
`;
127+
return dtsContent;
78128
}

packages/scaffold/src/utils/zotero-runner.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ import { delay, toMerged } from "es-toolkit";
77
import { ensureDir, outputFile, outputJSON, pathExists, readJSON, remove } from "fs-extra/esm";
88
import { isLinux, isMacOS, isWindows } from "std-env";
99
import { logger } from "./log.js";
10-
import { PrefsManager } from "./prefs.js";
10+
import { PrefsManager } from "./prefs-manager.js";
1111
import { isRunning } from "./process.js";
12+
import { prefs as defaultPrefs } from "./zotero/preference.js";
1213
import { findFreeTcpPort, RemoteFirefox } from "./zotero/remote-zotero.js";
1314

1415
export interface ZoteroRunnerOptions {
@@ -128,9 +129,10 @@ export class ZoteroRunner {
128129
// Setup prefs.js
129130
const prefsPath = join(this.options.profile.path, "prefs.js");
130131
const prefsManager = new PrefsManager("user_pref");
132+
prefsManager.setPrefs(defaultPrefs);
133+
prefsManager.setPrefs(this.options.profile.customPrefs);
131134
if (await pathExists(prefsPath))
132135
await prefsManager.read(prefsPath);
133-
prefsManager.setPrefs(this.options.profile.customPrefs);
134136
prefsManager.setPrefs({
135137
"extensions.lastAppBuildId": null,
136138
"extensions.lastAppVersion": null,

0 commit comments

Comments
 (0)