@nsnanocat/preference-panes 0.9.16 → 1.0.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/README.md +21 -15
- package/dist/api.js +1454 -967
- package/dist/module/app.mjs +296 -412
- package/dist/module/index.html +1 -1
- package/dist/module/navigation.mjs +7 -6
- package/dist/preference-panes.mjs +259 -374
- package/dist/web.js +1202 -0
- package/package.json +1 -1
- package/src/api.mjs +191 -0
- package/src/browser/ModuleStatus.mjs +7 -6
- package/src/browser/Navigation.d.mts +8 -6
- package/src/browser/app.mjs +5 -7
- package/src/{lib → browser}/boxjs.mjs +72 -16
- package/src/browser/client.d.mts +44 -129
- package/src/browser/client.mjs +61 -179
- package/src/browser/index.d.ts +7 -11
- package/src/browser/index.mjs +15 -7
- package/src/browser/panel.mjs +31 -22
- package/src/build.mjs +2 -3
- package/src/index.d.ts +16 -2
- package/src/web.mjs +52 -0
- package/src/BoxJS.mjs +0 -72
- package/src/Store.mjs +0 -114
- package/src/lib/response.mjs +0 -16
- package/src/proxy/handler.mjs +0 -39
- package/src/proxy/response.mjs +0 -16
package/src/BoxJS.mjs
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { validatePathParts } from "./lib/settings-path.mjs";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* BoxJS 的共同目录:模块、存储根和展示元数据都来自同一份 JSON。
|
|
5
|
-
* Shared BoxJS catalog deriving modules, storage roots and metadata from one JSON document.
|
|
6
|
-
*/
|
|
7
|
-
export class BoxJS {
|
|
8
|
-
/**
|
|
9
|
-
* 建立路径索引,不解析控件类型,也不读写持久化存储。
|
|
10
|
-
* Index field paths without interpreting controls or accessing persistence.
|
|
11
|
-
* @param {unknown} input 字段数组、单个 app 或 apps 订阅 / Field array, app or apps subscription.
|
|
12
|
-
*/
|
|
13
|
-
constructor(input) {
|
|
14
|
-
if (!input || typeof input !== "object") throw new TypeError("Expected BoxJS JSON");
|
|
15
|
-
const document = JSON.parse(JSON.stringify(input));
|
|
16
|
-
const apps = Array.isArray(document) ? [{ settings: document }] : (document.apps ?? [document]);
|
|
17
|
-
if (!Array.isArray(apps)) throw new TypeError("Expected BoxJS apps array");
|
|
18
|
-
this.modules = new Map();
|
|
19
|
-
for (const app of apps) {
|
|
20
|
-
if (!app || !Array.isArray(app.settings)) throw new TypeError("Expected BoxJS settings array");
|
|
21
|
-
for (const entry of app.settings) {
|
|
22
|
-
if (typeof entry.id !== "string") throw new TypeError("BoxJS settings require string IDs");
|
|
23
|
-
if (!entry.id.startsWith("@")) {
|
|
24
|
-
if (Array.isArray(document)) throw new TypeError("BoxJS settings require @root.path IDs");
|
|
25
|
-
continue;
|
|
26
|
-
}
|
|
27
|
-
const [storageKey, ...parts] = entry.id.slice(1).split(".");
|
|
28
|
-
if (!storageKey || storageKey.startsWith("@") || parts.length < 2) throw new TypeError("A BoxJS setting must be below a literal storage root and module");
|
|
29
|
-
validatePathParts(parts);
|
|
30
|
-
const module = parts[0];
|
|
31
|
-
let target = this.modules.get(module);
|
|
32
|
-
if (!target) {
|
|
33
|
-
target = { module, storageKey, entries: [], owners: new Set() };
|
|
34
|
-
this.modules.set(module, target);
|
|
35
|
-
}
|
|
36
|
-
if (target.storageKey !== storageKey) throw new TypeError(`A module must use one storage root: ${module}`);
|
|
37
|
-
target.entries.push(entry);
|
|
38
|
-
target.owners.add(app);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
this.metadata = metadata(Array.isArray(document) ? {} : document);
|
|
42
|
-
for (const target of this.modules.values()) target.metadata = target.owners.size === 1 ? metadata([...target.owners][0]) : {};
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* 取得本次导入的唯一模块,避免把模块数据变成项目目录。
|
|
47
|
-
* Get the single imported module without turning module data into a project directory.
|
|
48
|
-
* @returns {object} 唯一模块的目录项 / The single module entry.
|
|
49
|
-
*/
|
|
50
|
-
get module() {
|
|
51
|
-
if (this.modules.size !== 1) throw new TypeError("Import BoxJS JSON for exactly one module");
|
|
52
|
-
return this.modules.values().next().value;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* 保留标准 BoxJS 展示信息;script 仅为元数据,不执行。
|
|
58
|
-
* Retain standard BoxJS presentation data; script is metadata only and never executed.
|
|
59
|
-
* @param {object} source BoxJS app 或订阅 / BoxJS app or subscription.
|
|
60
|
-
* @returns {object} 经过类型检查的展示信息 / Type-checked presentation metadata.
|
|
61
|
-
*/
|
|
62
|
-
function metadata(source) {
|
|
63
|
-
const result = {};
|
|
64
|
-
for (const key of ["id", "name", "author", "repo", "script", "icon", "description", "desc", "icons", "descs"]) {
|
|
65
|
-
if (source[key] === undefined) continue;
|
|
66
|
-
const multiple = key === "icons" || key === "descs";
|
|
67
|
-
const values = multiple ? source[key] : [source[key]];
|
|
68
|
-
if (!Array.isArray(values) || values.some(item => typeof item !== "string")) throw new TypeError(`Invalid BoxJS app ${key}`);
|
|
69
|
-
result[key] = multiple ? [...values] : source[key];
|
|
70
|
-
}
|
|
71
|
-
return result;
|
|
72
|
-
}
|
package/src/Store.mjs
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { URL } from "@nsnanocat/url";
|
|
2
|
-
import { Lodash as _ } from "@nsnanocat/util/polyfill/Lodash.mjs";
|
|
3
|
-
import { Storage } from "@nsnanocat/util/polyfill/Storage";
|
|
4
|
-
import { response } from "./lib/response.mjs";
|
|
5
|
-
import { validatePathParts } from "./lib/settings-path.mjs";
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* 无配置绑定的本地存储桥接;form 字段名就是完整 @root.path。
|
|
9
|
-
* Unbound local storage bridge; the form field name is the complete @root.path.
|
|
10
|
-
*/
|
|
11
|
-
export class Store {
|
|
12
|
-
/**
|
|
13
|
-
* POST /api/get、set、delete;不下载配置、不解析控件、不鉴权。
|
|
14
|
-
* POST /api/get, set or delete without config downloads, control parsing or authentication.
|
|
15
|
-
* @param {import("./index.js").SettingsRequest} request 代理请求 / Proxy request.
|
|
16
|
-
* @param {URL} [url] 已解析地址 / Parsed URL.
|
|
17
|
-
* @returns {Promise<import("./index.js").SettingsResponse | undefined>} 操作结果 / Operation result.
|
|
18
|
-
*/
|
|
19
|
-
async handle(request, url = new URL(request.url)) {
|
|
20
|
-
if (!url.pathname.startsWith("/api/")) return;
|
|
21
|
-
const reply = (status, data) => response(request, status, data);
|
|
22
|
-
const action = url.pathname.slice(5);
|
|
23
|
-
if (!["get", "set", "delete"].includes(action)) return reply(404, { error: "Unknown action" });
|
|
24
|
-
if (request.method !== "POST") return reply(405, { error: "Use POST with a form body" });
|
|
25
|
-
const headers = Object.fromEntries(Object.entries(request.headers ?? {}).map(([key, value]) => [key.toLowerCase(), value]));
|
|
26
|
-
if (headers["content-type"]?.split(";")[0].trim().toLowerCase() !== "application/x-www-form-urlencoded") return reply(415, { error: "Expected application/x-www-form-urlencoded" });
|
|
27
|
-
if (typeof request.body !== "string" || request.body.length > 65536) return reply(400, { error: "Expected a form body up to 65536 characters" });
|
|
28
|
-
let parts, value;
|
|
29
|
-
try {
|
|
30
|
-
const fields = request.body.split("&");
|
|
31
|
-
if (fields.length !== 1) throw new TypeError("Send exactly one storage key");
|
|
32
|
-
const separator = fields[0].indexOf("=");
|
|
33
|
-
if (separator < 0) throw new TypeError("Expected @root.path=value");
|
|
34
|
-
const key = decodeURIComponent(fields[0].slice(0, separator).replace(/\+/g, " "));
|
|
35
|
-
value = decodeURIComponent(fields[0].slice(separator + 1).replace(/\+/g, " "));
|
|
36
|
-
if (!key.startsWith("@")) throw new TypeError("Storage keys must start with @");
|
|
37
|
-
parts = validatePathParts(key.slice(1).split("."));
|
|
38
|
-
if (parts.length < 2) throw new TypeError("Specify a storage root and child path");
|
|
39
|
-
} catch (error) {
|
|
40
|
-
return reply(400, { error: error.message });
|
|
41
|
-
}
|
|
42
|
-
if (action === "set") {
|
|
43
|
-
try {
|
|
44
|
-
value = JSON.parse(value);
|
|
45
|
-
} catch (error) {
|
|
46
|
-
if (!(error instanceof SyntaxError)) throw error;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
const [storageKey, ...path] = parts;
|
|
50
|
-
try {
|
|
51
|
-
const root = Storage.getItem(storageKey, {});
|
|
52
|
-
if (!isRecord(root)) throw new TypeError("Stored root must be an object");
|
|
53
|
-
const parent = storageParent(root, path, action === "set");
|
|
54
|
-
const key = path.at(-1);
|
|
55
|
-
switch (action) {
|
|
56
|
-
case "get": {
|
|
57
|
-
const result = parent ? _.get(parent, [key]) : undefined;
|
|
58
|
-
return result === undefined ? reply(404, { error: "Stored path does not exist" }) : reply(200, result);
|
|
59
|
-
}
|
|
60
|
-
case "set":
|
|
61
|
-
_.set(parent, [key], value);
|
|
62
|
-
break;
|
|
63
|
-
case "delete":
|
|
64
|
-
if (parent) _.unset(parent, [key]);
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
if (!Storage.setItem(storageKey, root)) throw new Error("Storage write failed");
|
|
68
|
-
return reply(200, action === "set" ? { saved: true } : { deleted: true });
|
|
69
|
-
} catch (error) {
|
|
70
|
-
return reply(500, { error: error.message });
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* 判断根节点是否为普通对象。
|
|
77
|
-
* Determine whether a root node is a plain object.
|
|
78
|
-
* @param {unknown} value 待检查值 / Value to inspect.
|
|
79
|
-
* @returns {boolean} 是否为普通对象 / Whether this is a plain object.
|
|
80
|
-
*/
|
|
81
|
-
function isRecord(value) {
|
|
82
|
-
return value !== null && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* 遍历父路径,兼容旧存储中 JSON 字符串形式的中间节点。
|
|
87
|
-
* Traverse parents, supporting legacy intermediate nodes serialized as JSON strings.
|
|
88
|
-
* @param {Record<string, unknown>} root 存储根 / Storage root.
|
|
89
|
-
* @param {string[]} parts 完整路径 / Complete path.
|
|
90
|
-
* @param {boolean} create 是否创建缺失节点 / Whether to create missing parents.
|
|
91
|
-
* @returns {object | undefined} 父节点,缺失且不创建时为 undefined / Parent, or undefined when absent and not creating.
|
|
92
|
-
* @throws {TypeError} 无法继续遍历标量节点 / A scalar node cannot be traversed.
|
|
93
|
-
*/
|
|
94
|
-
function storageParent(root, parts, create) {
|
|
95
|
-
let parent = root;
|
|
96
|
-
for (const part of parts.slice(0, -1)) {
|
|
97
|
-
let next = _.get(parent, [part]);
|
|
98
|
-
switch (typeof next) {
|
|
99
|
-
case "undefined":
|
|
100
|
-
if (!create) return;
|
|
101
|
-
next = {};
|
|
102
|
-
break;
|
|
103
|
-
case "string":
|
|
104
|
-
next = JSON.parse(next);
|
|
105
|
-
break;
|
|
106
|
-
default:
|
|
107
|
-
break;
|
|
108
|
-
}
|
|
109
|
-
if (!isRecord(next) && !Array.isArray(next)) throw new TypeError("Stored parent is not an object or array");
|
|
110
|
-
_.set(parent, [part], next);
|
|
111
|
-
parent = next;
|
|
112
|
-
}
|
|
113
|
-
return parent;
|
|
114
|
-
}
|
package/src/lib/response.mjs
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 统一生成不可缓存的响应,HEAD 始终省略正文。
|
|
3
|
-
* Create an uncached response, always omitting the body for HEAD.
|
|
4
|
-
* @param {import("../index.js").SettingsRequest} request 宿主请求 / Host request.
|
|
5
|
-
* @param {number} status HTTP 状态 / HTTP status.
|
|
6
|
-
* @param {unknown} body JSON 数据或资源正文 / JSON data or resource body.
|
|
7
|
-
* @param {string} [type] 媒体类型 / Media type.
|
|
8
|
-
* @returns {import("../index.js").SettingsResponse} 通用响应 / Common response.
|
|
9
|
-
*/
|
|
10
|
-
export function response(request, status, body, type = "application/json") {
|
|
11
|
-
return {
|
|
12
|
-
status,
|
|
13
|
-
headers: { "Content-Type": `${type}; charset=utf-8`, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" },
|
|
14
|
-
body: request.method === "HEAD" ? "" : type === "application/json" ? JSON.stringify(body) : body,
|
|
15
|
-
};
|
|
16
|
-
}
|
package/src/proxy/handler.mjs
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { URL } from "@nsnanocat/url";
|
|
2
|
-
import assets from "#assets";
|
|
3
|
-
import { pageInputs } from "../lib/page-inputs.mjs";
|
|
4
|
-
import { response } from "../lib/response.mjs";
|
|
5
|
-
import { Store } from "../Store.mjs";
|
|
6
|
-
import { complete } from "./response.mjs";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 通用代理入口:提供无鉴权 form 存储 API 及模块页面,不含业务配置。
|
|
10
|
-
* Generic proxy entry serving unauthenticated form storage and module pages without business configuration.
|
|
11
|
-
* @returns {Promise<void>} 已交给代理宿主的响应 / Response delivered to the proxy host.
|
|
12
|
-
*/
|
|
13
|
-
async function run() {
|
|
14
|
-
const request = globalThis.$request;
|
|
15
|
-
let result;
|
|
16
|
-
try {
|
|
17
|
-
const url = new URL(request.url);
|
|
18
|
-
switch (true) {
|
|
19
|
-
case url.pathname.startsWith("/api/"):
|
|
20
|
-
result = await new Store().handle(request, url);
|
|
21
|
-
break;
|
|
22
|
-
case /^\/settings\/[a-zA-Z0-9_-]+\/?$/.test(url.pathname): {
|
|
23
|
-
const inputs = encodeURIComponent(JSON.stringify(pageInputs(url, request.headers)));
|
|
24
|
-
result = response(request, 200, assets.page.body.replace("</head>", `<meta name="preference-panes-inputs" content="${inputs}"></head>`), "text/html");
|
|
25
|
-
break;
|
|
26
|
-
}
|
|
27
|
-
default: {
|
|
28
|
-
const asset = assets[url.pathname];
|
|
29
|
-
if (asset) result = response(request, 200, asset.body, asset.type);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
if (result && !url.pathname.startsWith("/api/") && !["GET", "HEAD"].includes(request.method)) result = response(request, 405, { error: "Method not allowed" });
|
|
33
|
-
} catch (error) {
|
|
34
|
-
console.error(`PreferencePanes: ${error.message}`);
|
|
35
|
-
result = response(request, 500, { error: error.message });
|
|
36
|
-
}
|
|
37
|
-
complete(result);
|
|
38
|
-
}
|
|
39
|
-
run();
|
package/src/proxy/response.mjs
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { $app } from "@nsnanocat/util/lib/app.mjs";
|
|
2
|
-
import { done } from "@nsnanocat/util/lib/done.mjs";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 统一适配代理的完成格式;未接管的请求原样继续。
|
|
6
|
-
* Adapt the host completion format and pass through unhandled requests.
|
|
7
|
-
* @param {import("../index.js").SettingsResponse | undefined} result 通用响应 / Common response.
|
|
8
|
-
* @returns {void} 响应已交给宿主 / Response delivered to the host.
|
|
9
|
-
*/
|
|
10
|
-
export function complete(result) {
|
|
11
|
-
if (!result) {
|
|
12
|
-
done({});
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
done($app === "Quantumult X" ? result : { response: result });
|
|
16
|
-
}
|