@nsnanocat/preference-panes 0.1.0 → 0.2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nsnanocat/preference-panes",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Shared settings API runtime for JavaScript proxy modules",
5
5
  "author": "VirgilClyne <Virgil@nanocat.me>",
6
6
  "homepage": "https://NSNanoCat.github.io/preference-panes",
@@ -16,21 +16,21 @@
16
16
  "license": "Apache-2.0",
17
17
  "bugs": "https://github.com/NSNanoCat/PreferencePanes/issues",
18
18
  "type": "module",
19
- "main": "index.mjs",
19
+ "main": "src/index.mjs",
20
20
  "exports": {
21
21
  ".": {
22
- "types": "./types/index.d.ts",
23
- "import": "./index.mjs"
22
+ "types": "./src/index.d.ts",
23
+ "import": "./src/index.mjs"
24
24
  },
25
25
  "./browser": {
26
- "types": "./types/browser.d.ts",
27
- "import": "./browser/index.mjs"
26
+ "types": "./src/browser/index.d.ts",
27
+ "import": "./src/browser/index.mjs"
28
28
  },
29
- "./browser/panel.css": "./browser/panel.css",
29
+ "./browser/panel.css": "./src/browser/panel.css",
30
30
  "./dist/preference-panes.mjs": "./dist/preference-panes.mjs",
31
31
  "./dist/preference-panes.request.js": "./dist/preference-panes.request.js"
32
32
  },
33
- "types": "types/index.d.ts",
33
+ "types": "src/index.d.ts",
34
34
  "scripts": {
35
35
  "test": "node --test test/*.test.mjs",
36
36
  "check": "npm run lint && npm run typecheck && npm test",
@@ -39,19 +39,16 @@
39
39
  "typecheck": "tsc --noEmit",
40
40
  "build": "rollup -c",
41
41
  "prepack": "npm run build",
42
- "apifox:generate": "node scripts/generate-apifox.mjs"
42
+ "apifox:generate": "node apifox/generate.mjs",
43
+ "apifox:check": "node apifox/generate.mjs --check"
43
44
  },
44
45
  "repository": {
45
46
  "type": "git",
46
47
  "url": "git+https://github.com/NSNanoCat/PreferencePanes.git"
47
48
  },
48
49
  "files": [
49
- "index.mjs",
50
- "lib",
51
- "browser",
52
- "types",
53
- "dist",
54
- "proxy"
50
+ "src",
51
+ "dist"
55
52
  ],
56
53
  "dependencies": {
57
54
  "@nsnanocat/url": "^1.2.6",
@@ -1,23 +1,48 @@
1
+ import { URL } from "@nsnanocat/url";
2
+ import { fetch } from "@nsnanocat/util/polyfill/fetch";
1
3
  import { Lodash as _ } from "@nsnanocat/util/polyfill/Lodash.mjs";
2
4
  import { Storage } from "@nsnanocat/util/polyfill/Storage";
3
- import { normalizeBoxJs, validValue } from "./boxjs.mjs";
4
- import { parseSettingsPath } from "./settings-path.mjs";
5
+ import { normalizeBoxJs, validValue } from "./lib/boxjs.mjs";
6
+ import { parseSettingsPath } from "./lib/settings-path.mjs";
5
7
 
6
8
  /**
7
- * 创建通用读写处理器;字段通过 loadConfig 在运行时加载,不固化在脚本中。
8
- * Create a generic endpoint using runtime-loaded config, never compiled-in fields.
9
- * @param {import("../types/index.js").SettingsHandlerOptions} options 路由与配置加载器 / Routing and config loader.
10
- * @returns {(request: import("../types/index.js").SettingsRequest) => Promise<import("../types/index.js").SettingsResponse | undefined>} 异步处理器 / Async handler.
9
+ * 使用 util 下载 BoxJS、校验字段并读写持久化存储。
10
+ * Download BoxJS through util, validate fields and handle persistent storage.
11
11
  */
12
- export function createSettingsHandler({ origin, loadConfig, requestHeader = "X-Settings-Client", resolveSettings }) {
13
- const target = new URL(origin);
14
- if (target.protocol !== "https:" || target.pathname !== "/" || target.search || target.hash || target.username || target.password)
15
- throw new TypeError("origin must be an HTTPS origin");
16
- if (typeof loadConfig !== "function") throw new TypeError("loadConfig is required");
17
- if (!/^[a-z][a-z0-9-]*$/i.test(requestHeader)) throw new TypeError("Invalid requestHeader");
18
- return async function handle(request) {
12
+ export class SettingsHandler {
13
+ #origin;
14
+ #configURL;
15
+ #requestHeader;
16
+ #resolveSettings;
17
+
18
+ /** @param {import("./index.js").SettingsHandlerOptions} options 来源、配置地址与 GET 解析器 / Origin, config source and GET resolver. */
19
+ constructor({ origin, configURL, requestHeader = "X-Settings-Client", resolveSettings }) {
20
+ const target = new URL(origin);
21
+ if (target.protocol !== "https:" || target.pathname !== "/" || target.search || target.hash || target.username || target.password)
22
+ throw new TypeError("origin must be an HTTPS origin");
23
+ const source = new URL(configURL);
24
+ if (source.protocol !== "https:" || source.username || source.password || source.hash)
25
+ throw new TypeError("configURL must be an HTTPS URL without credentials or fragment");
26
+ if (!/^[a-z][a-z0-9-]*$/i.test(requestHeader)) throw new TypeError("Invalid requestHeader");
27
+ this.#origin = target.origin;
28
+ this.#configURL = source.href;
29
+ this.#requestHeader = requestHeader;
30
+ this.#resolveSettings = resolveSettings;
31
+ }
32
+
33
+ async #loadConfig(module) {
34
+ const response = await fetch({ url: this.#configURL, method: "GET", headers: { "Cache-Control": "no-cache" }, timeout: 5000 });
35
+ if (response.status !== 200) throw new Error(`BoxJS source HTTP ${response.status}`);
36
+ return normalizeBoxJs(JSON.parse(response.body), module);
37
+ }
38
+
39
+ /**
40
+ * @param {import("./index.js").SettingsRequest} request 代理请求 / Proxy request.
41
+ * @returns {Promise<import("./index.js").SettingsResponse | undefined>} API 响应,非本来源 API 则不处理 / API response, or undefined outside the configured API origin.
42
+ */
43
+ async handle(request) {
19
44
  const url = new URL(request.url);
20
- if (url.origin !== target.origin || !url.pathname.startsWith("/api/")) return;
45
+ if (url.origin !== this.#origin || !url.pathname.startsWith("/api/")) return;
21
46
  const headers = { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" };
22
47
  const reply = (status, data) => ({ status, headers, body: request.method === "HEAD" ? "" : JSON.stringify(data) });
23
48
  let parts;
@@ -27,13 +52,13 @@ export function createSettingsHandler({ origin, loadConfig, requestHeader = "X-S
27
52
  return reply(400, { error: error.message });
28
53
  }
29
54
  const requestHeaders = Object.fromEntries(Object.entries(request.headers ?? {}).map(([key, value]) => [key.toLowerCase(), value]));
30
- if (requestHeaders[requestHeader.toLowerCase()] !== "1" || (requestHeaders.origin && requestHeaders.origin !== target.origin))
55
+ if (requestHeaders[this.#requestHeader.toLowerCase()] !== "1" || (requestHeaders.origin && requestHeaders.origin !== this.#origin))
31
56
  return reply(403, { error: "Forbidden settings client" });
32
57
  if (!["HEAD", "GET", "POST", "DELETE"].includes(request.method))
33
58
  return { ...reply(405, { error: "Method not allowed" }), headers: { ...headers, Allow: "HEAD, GET, POST, DELETE" } };
34
59
  let definition;
35
60
  try {
36
- definition = normalizeBoxJs(await loadConfig(parts[0]), parts[0]);
61
+ definition = await this.#loadConfig(parts[0]);
37
62
  } catch (error) {
38
63
  return reply(502, { error: `Module configuration unavailable: ${error.message}` });
39
64
  }
@@ -44,7 +69,7 @@ export function createSettingsHandler({ origin, loadConfig, requestHeader = "X-S
44
69
  if (request.method === "HEAD") return reply(200, undefined);
45
70
  if (request.method === "GET") {
46
71
  const stored = Storage.getItem(definition.storageKey, {});
47
- const effective = resolveSettings ? resolveSettings(stored, definition) : stored;
72
+ const effective = this.#resolveSettings ? this.#resolveSettings(stored, definition) : stored;
48
73
  if (!isRecord(effective)) throw new TypeError("resolved settings must be a synchronous object");
49
74
  if (field) {
50
75
  const value = pathValue(effective, parts);
@@ -83,7 +108,7 @@ export function createSettingsHandler({ origin, loadConfig, requestHeader = "X-S
83
108
  }
84
109
  if (!Storage.setItem(definition.storageKey, saved)) return reply(500, { error: "Settings storage write failed" });
85
110
  return reply(200, request.method === "DELETE" ? { deleted: true } : { saved: true });
86
- };
111
+ }
87
112
  }
88
113
 
89
114
  function isRecord(value) {
@@ -5,8 +5,8 @@ import { parseSettingsPath } from "../lib/settings-path.mjs";
5
5
  /**
6
6
  * 创建页面会话缓存;打开时重读,选项操作仅在 HTTP 200 后更新缓存。
7
7
  * Create a page-session cache; reload on open and mutate cache only after HTTP 200.
8
- * @param {import("../types/browser.js").PreferencesClientOptions} options 请求与通知 / Requests and notifications.
9
- * @returns {import("../types/browser.js").PreferencesClient} 通用客户端 / Generic client.
8
+ * @param {import("./index.js").PreferencesClientOptions} options 请求与通知 / Requests and notifications.
9
+ * @returns {import("./index.js").PreferencesClient} 通用客户端 / Generic client.
10
10
  */
11
11
  export function createPreferencesClient({ fetch: request = globalThis.fetch.bind(globalThis), notify = () => {}, timeout = 10000 } = {}) {
12
12
  const sessions = new Map();
@@ -1,4 +1,4 @@
1
- import type { ModuleDefinition, SettingsScalar } from "./index.js";
1
+ import type { ModuleDefinition, SettingsScalar } from "../index.js";
2
2
  export interface Notification {
3
3
  kind: "success" | "error";
4
4
  operation: "write" | "delete";
@@ -21,6 +21,27 @@
21
21
  .pp-title {
22
22
  font-size: 18px;
23
23
  margin: 0;
24
+ overflow-wrap: anywhere;
25
+ min-width: 0;
26
+ }
27
+ .pp-module-info {
28
+ display: flex;
29
+ gap: 12px;
30
+ margin-bottom: 16px;
31
+ }
32
+ .pp-module-icon {
33
+ width: 48px;
34
+ height: 48px;
35
+ flex: none;
36
+ object-fit: contain;
37
+ }
38
+ .pp-module-details {
39
+ min-width: 0;
40
+ overflow-wrap: anywhere;
41
+ }
42
+ .pp-module-source {
43
+ color: inherit;
44
+ text-decoration: underline;
24
45
  }
25
46
  .pp-viewport {
26
47
  max-height: 80vh;
@@ -52,6 +73,8 @@
52
73
  font-size: 13px;
53
74
  opacity: 0.7;
54
75
  margin: 0 0 8px;
76
+ white-space: pre-wrap;
77
+ overflow-wrap: anywhere;
55
78
  }
56
79
  .pp-input:not([type="checkbox"]) {
57
80
  width: 100%;
@@ -3,7 +3,7 @@ import { createPreferencesClient } from "./client.mjs";
3
3
  /**
4
4
  * 挂载从 BoxJS 实时生成的设置面板和短暂通知。
5
5
  * Mount runtime-generated BoxJS controls and transient notifications.
6
- * @param {import("../types/browser.js").PreferencesPanelOptions} options 容器与请求;页面路径 /settings/{module} 对应配置 / Container and requests; /settings/{module} selects config.
6
+ * @param {import("./index.js").PreferencesPanelOptions} options 容器与请求;页面路径 /settings/{module} 对应配置 / Container and requests; /settings/{module} selects config.
7
7
  * @returns {{destroy(): void}} 清理接口 / Cleanup handle.
8
8
  */
9
9
  export function mountPreferencePanes({ element: root, fetch, title = "Preferences" }) {
@@ -78,7 +78,38 @@ export function mountPreferencePanes({ element: root, fetch, title = "Preference
78
78
  }
79
79
  function controls() {
80
80
  const { definition, values } = client.snapshot(active);
81
+ heading.textContent = definition.metadata?.name || active;
81
82
  const view = node("section", "pp-fields");
83
+ const growingInputs = [];
84
+ const metadata = definition.metadata;
85
+ if (metadata) {
86
+ const info = node("div", "pp-module-info");
87
+ const iconURL = metadata.icon || metadata.icons?.[1] || metadata.icons?.[0];
88
+ const resourceURL = (value) => {
89
+ const url = new window.URL(value, window.location.href);
90
+ if (!["http:", "https:"].includes(url.protocol)) throw new TypeError("Module metadata URLs must use HTTP or HTTPS");
91
+ return url.href;
92
+ };
93
+ if (iconURL) {
94
+ const image = node("img", "pp-module-icon");
95
+ image.src = resourceURL(iconURL);
96
+ image.alt = "";
97
+ info.append(image);
98
+ }
99
+ const details = node("div", "pp-module-details");
100
+ if (metadata.author) details.append(node("p", "pp-description", metadata.author));
101
+ for (const description of [metadata.desc ?? metadata.description, ...(metadata.descs ?? [])])
102
+ if (description) details.append(node("p", "pp-description", description));
103
+ if (metadata.repo) {
104
+ const link = node("a", "pp-module-source", "项目主页");
105
+ link.href = resourceURL(metadata.repo);
106
+ link.target = "_blank";
107
+ link.rel = "noopener noreferrer";
108
+ details.append(link);
109
+ }
110
+ info.append(details);
111
+ view.append(info);
112
+ }
82
113
  for (const field of definition.fields) {
83
114
  const row = node("fieldset", "pp-field");
84
115
  row.append(node("legend", "", field.name));
@@ -113,8 +144,23 @@ export function mountPreferencePanes({ element: root, fetch, title = "Preference
113
144
  for (const option of inputs) option.input.checked = Array.isArray(value) && value.includes(option.key);
114
145
  };
115
146
  } else {
116
- const input = node(field.type === "array" ? "textarea" : "input", "pp-input");
147
+ const multiline = field.control === "textarea" || field.type === "array";
148
+ const input = node(multiline ? "textarea" : "input", "pp-input");
117
149
  input.setAttribute("aria-label", field.name);
150
+ if (field.placeholder) input.placeholder = field.placeholder;
151
+ if (multiline && field.rows) input.rows = field.rows;
152
+ const grow = () => {
153
+ if (!multiline || !field.autoGrow || !input.isConnected) return;
154
+ input.style.height = "auto";
155
+ const baseline = input.getBoundingClientRect().height;
156
+ const style = window.getComputedStyle(input);
157
+ const borders = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);
158
+ input.style.height = `${Math.max(baseline, input.scrollHeight + borders)}px`;
159
+ };
160
+ if (multiline && field.autoGrow) {
161
+ input.addEventListener("input", grow);
162
+ growingInputs.push(grow);
163
+ }
118
164
  if (field.type === "boolean") {
119
165
  input.type = "checkbox";
120
166
  write = (value) => {
@@ -122,9 +168,10 @@ export function mountPreferencePanes({ element: root, fetch, title = "Preference
122
168
  };
123
169
  read = () => input.checked;
124
170
  } else {
125
- input.type = field.type === "number" ? "number" : "text";
171
+ if (!multiline) input.type = field.type === "number" ? "number" : "text";
126
172
  write = (value) => {
127
173
  input.value = field.type === "array" ? JSON.stringify(value ?? []) : (value ?? "");
174
+ grow();
128
175
  };
129
176
  read = () =>
130
177
  field.type === "array"
@@ -188,6 +235,7 @@ export function mountPreferencePanes({ element: root, fetch, title = "Preference
188
235
  view.append(row);
189
236
  }
190
237
  viewport.replaceChildren(view);
238
+ for (const grow of growingInputs) grow();
191
239
  }
192
240
  function route() {
193
241
  if (saving) {
@@ -8,6 +8,10 @@ interface FieldBase {
8
8
  key: string;
9
9
  name: string;
10
10
  description?: string;
11
+ control?: "boolean" | "checkboxes" | "selects" | "text" | "textarea" | "number";
12
+ placeholder?: string;
13
+ rows?: number;
14
+ autoGrow?: boolean;
11
15
  }
12
16
  /** 对齐 argument 配置字段 / Argument-compatible field. */
13
17
  export type SettingsField = FieldBase &
@@ -32,8 +36,8 @@ export interface SettingsResponse {
32
36
  export interface SettingsHandlerOptions {
33
37
  /** 接管 /api/ 路径的 HTTPS 来源 / HTTPS origin serving /api/ paths. */
34
38
  origin: string;
35
- /** 运行时加载 BoxJS JSON / Load BoxJS JSON at runtime. */
36
- loadConfig: (module: string) => unknown | Promise<unknown>;
39
+ /** BoxJS JSON 的 HTTPS 下载地址;类内部使用 util fetch 加载 / HTTPS BoxJS source fetched by the class through util. */
40
+ configURL: string;
37
41
  /** 页面专用请求头,值为 1 / Dedicated header, value 1. */
38
42
  requestHeader?: string;
39
43
  /** 每次 GET 解析有效设置,默认读取持久化值;优先级由调用方决定。
@@ -45,8 +49,24 @@ export interface ModuleDefinition {
45
49
  storageKey: string;
46
50
  fields: SettingsField[];
47
51
  settingsPath: string[];
52
+ metadata?: {
53
+ id?: string;
54
+ name?: string;
55
+ author?: string;
56
+ repo?: string;
57
+ /** 仅保留来源信息,不执行脚本 / Source metadata only; never executed. */
58
+ script?: string;
59
+ icon?: string;
60
+ icons?: string[];
61
+ descs?: string[];
62
+ description?: string;
63
+ desc?: string;
64
+ };
48
65
  }
49
66
  export function normalizeBoxJs(config: unknown, module: string): ModuleDefinition;
50
- export function createSettingsHandler(options: SettingsHandlerOptions): (request: SettingsRequest) => Promise<SettingsResponse | undefined>;
67
+ export class SettingsHandler {
68
+ constructor(options: SettingsHandlerOptions);
69
+ handle(request: SettingsRequest): Promise<SettingsResponse | undefined>;
70
+ }
51
71
  /** 解析 /api/ 后的 database 路径;非法路径抛错 / Parse database path after /api/; throws on unsafe paths. */
52
72
  export function parseSettingsPath(url: string): string[] | undefined;
@@ -1,3 +1,3 @@
1
1
  export { normalizeBoxJs } from "./lib/boxjs.mjs";
2
- export { createSettingsHandler } from "./lib/settings-handler.mjs";
3
2
  export { parseSettingsPath } from "./lib/settings-path.mjs";
3
+ export { SettingsHandler } from "./SettingsHandler.mjs";
@@ -5,11 +5,23 @@ import { parseSettingsPath } from "./settings-path.mjs";
5
5
  * Normalize a BoxJS array, app or subscription using the source JSON as the field authority.
6
6
  * @param {unknown} config BoxJS JSON / BoxJS document.
7
7
  * @param {string} module API 第一段模块名 / First API path segment.
8
- * @returns {import("../types/index.js").ModuleDefinition} 存储根和字段 / Storage root and fields.
8
+ * @returns {import("../index.js").ModuleDefinition} 存储根和字段 / Storage root and fields.
9
9
  */
10
10
  export function normalizeBoxJs(config, module) {
11
11
  parseSettingsPath(`https://example.invalid/api/${module}`);
12
- const entries = Array.isArray(config) ? config : config?.apps ? config.apps.flatMap((app) => app.settings ?? []) : config?.settings;
12
+ const apps = Array.isArray(config) ? [] : (config?.apps ?? [config]);
13
+ if (!Array.isArray(apps)) throw new TypeError("Expected BoxJS apps array");
14
+ for (const candidate of apps) {
15
+ if (!candidate || typeof candidate !== "object") throw new TypeError("Expected BoxJS app object");
16
+ if (candidate.settings !== undefined && !Array.isArray(candidate.settings)) throw new TypeError("Expected BoxJS settings array");
17
+ }
18
+ const owners = apps.filter((candidate) =>
19
+ candidate.settings?.some(
20
+ (entry) => typeof entry.id === "string" && entry.id.startsWith("@") && entry.id.slice(1).split(".")[1] === module,
21
+ ),
22
+ );
23
+ const entries = Array.isArray(config) ? config : owners.flatMap((candidate) => candidate.settings);
24
+ const app = owners.length === 1 ? owners[0] : undefined;
13
25
  if (!Array.isArray(entries)) throw new TypeError("Expected BoxJS settings array, app or subscription");
14
26
  let storageKey;
15
27
  const fields = [];
@@ -30,6 +42,10 @@ export function normalizeBoxJs(config, module) {
30
42
  name: entry.name,
31
43
  type: type === "select" ? typeof entry.val : type,
32
44
  description: entry.desc ?? "",
45
+ control: entry.type,
46
+ ...(entry.placeholder === undefined ? {} : { placeholder: entry.placeholder }),
47
+ ...(entry.rows === undefined ? {} : { rows: entry.rows }),
48
+ ...(entry.autoGrow === undefined ? {} : { autoGrow: entry.autoGrow }),
33
49
  };
34
50
  if (type === "select" && !["string", "number", "boolean"].includes(field.type))
35
51
  throw new TypeError(`Select requires a scalar val: ${entry.id}`);
@@ -37,6 +53,9 @@ export function normalizeBoxJs(config, module) {
37
53
  if (Object.hasOwn(entry, "val")) field.defaultValue = normalizeStoredValue(field, entry.val);
38
54
  if (
39
55
  typeof field.name !== "string" ||
56
+ (field.placeholder !== undefined && typeof field.placeholder !== "string") ||
57
+ (field.rows !== undefined && (!Number.isInteger(field.rows) || field.rows < 1)) ||
58
+ (field.autoGrow !== undefined && typeof field.autoGrow !== "boolean") ||
40
59
  fields.some((other) => other.key === field.key || other.key.startsWith(`${field.key}.`) || field.key.startsWith(`${other.key}.`))
41
60
  )
42
61
  throw new TypeError(`Invalid or overlapping BoxJS field: ${entry.id}`);
@@ -53,13 +72,32 @@ export function normalizeBoxJs(config, module) {
53
72
  if (!fields.length) throw new TypeError(`No BoxJS settings for module: ${module}`);
54
73
  const common = fields[0].key.split(".").slice(0, -1);
55
74
  for (const field of fields) while (!field.key.startsWith(`${common.join(".")}.`)) common.pop();
56
- return { module, storageKey, fields, settingsPath: common };
75
+ const metadata = {};
76
+ if (app) {
77
+ for (const key of ["id", "name", "author", "repo", "script", "icon", "description", "desc"]) {
78
+ if (app[key] === undefined) continue;
79
+ if (typeof app[key] !== "string") throw new TypeError(`Invalid BoxJS app ${key}`);
80
+ metadata[key] = app[key];
81
+ }
82
+ for (const key of ["icons", "descs"]) {
83
+ if (app[key] === undefined) continue;
84
+ if (!Array.isArray(app[key]) || app[key].some((item) => typeof item !== "string")) throw new TypeError(`Invalid BoxJS app ${key}`);
85
+ metadata[key] = [...app[key]];
86
+ }
87
+ }
88
+ return {
89
+ module,
90
+ storageKey,
91
+ fields,
92
+ settingsPath: common,
93
+ ...(Object.keys(metadata).length ? { metadata } : {}),
94
+ };
57
95
  }
58
96
 
59
97
  /**
60
98
  * 归一化 BoxJS 的字符串存储值,不改变普通文本内容。
61
99
  * Normalize BoxJS string persistence without changing free-text values.
62
- * @param {import("../types/index.js").SettingsField} field 字段 / Field.
100
+ * @param {import("../index.js").SettingsField} field 字段 / Field.
63
101
  * @param {unknown} value 存储值 / Stored value.
64
102
  * @returns {unknown} 控件值 / Control value.
65
103
  */
@@ -1,3 +1,5 @@
1
+ import { URL } from "@nsnanocat/url";
2
+
1
3
  /**
2
4
  * 将 /api/ 后的 URL 路径转换为 util 的路径片段;非 API 路径不处理。
3
5
  * Convert URL segments after /api/ to util path segments; ignore non-API paths.
@@ -0,0 +1,21 @@
1
+ import { $app } from "@nsnanocat/util/lib/app.mjs";
2
+ import { done } from "@nsnanocat/util/lib/done.mjs";
3
+ import { qs } from "@nsnanocat/util/polyfill/qs.mjs";
4
+ import { SettingsHandler } from "../SettingsHandler.mjs";
5
+
6
+ (async () => {
7
+ let response;
8
+ try {
9
+ const { origin, configURL } = qs.parse(globalThis.$argument);
10
+ const handler = new SettingsHandler({ origin, configURL });
11
+ response = await handler.handle(globalThis.$request);
12
+ } catch (error) {
13
+ console.error(`PreferencePanes: ${error.message}`);
14
+ response = {
15
+ status: 500,
16
+ headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" },
17
+ body: globalThis.$request.method === "HEAD" ? "" : JSON.stringify({ error: "Settings execution failed" }),
18
+ };
19
+ }
20
+ done(response ? ($app === "Quantumult X" ? response : { response }) : {});
21
+ })();
package/proxy/request.mjs DELETED
@@ -1,36 +0,0 @@
1
- import { URL } from "@nsnanocat/url";
2
- import { $app } from "@nsnanocat/util/lib/app.mjs";
3
- import { done } from "@nsnanocat/util/lib/done.mjs";
4
- import { fetch } from "@nsnanocat/util/polyfill/fetch";
5
- import { qs } from "@nsnanocat/util/polyfill/qs.mjs";
6
- import { createSettingsHandler } from "../lib/settings-handler.mjs";
7
-
8
- // JavaScriptCore does not provide the browser URL global.
9
- // JavaScriptCore 不提供浏览器的 URL 全局对象。
10
- globalThis.URL ??= URL;
11
-
12
- (async () => {
13
- let response;
14
- try {
15
- const { origin, configURL } = qs.parse(globalThis.$argument);
16
- const source = new globalThis.URL(configURL);
17
- if (source.protocol !== "https:") throw new TypeError("configURL must use HTTPS");
18
- const handle = createSettingsHandler({
19
- origin,
20
- loadConfig: async () => {
21
- const response = await fetch({ url: source.href, method: "GET", headers: { "Cache-Control": "no-cache" }, timeout: 5000 });
22
- if (response.status !== 200) throw new Error(`BoxJS source HTTP ${response.status}`);
23
- return JSON.parse(response.body);
24
- },
25
- });
26
- response = await handle(globalThis.$request);
27
- } catch (error) {
28
- console.error(`PreferencePanes: ${error.message}`);
29
- response = {
30
- status: 500,
31
- headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store" },
32
- body: globalThis.$request.method === "HEAD" ? "" : JSON.stringify({ error: "Settings execution failed" }),
33
- };
34
- }
35
- done(response ? ($app === "Quantumult X" ? response : { response }) : {});
36
- })();
File without changes