@nsnanocat/preference-panes 1.1.1 → 1.1.2
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 +12 -14
- package/dist/api.js +114 -104
- package/dist/module/index.html +1 -1
- package/dist/module/index.mjs +124 -121
- package/dist/preference-panes.mjs +25 -24
- package/dist/web.js +1 -1
- package/package.json +1 -1
- package/src/api.mjs +117 -104
- package/src/browser/client.mjs +25 -24
- package/src/browser/index.mjs +7 -4
- package/src/index.d.ts +2 -2
package/src/api.mjs
CHANGED
|
@@ -2,14 +2,13 @@ import { URL } from "@nsnanocat/url";
|
|
|
2
2
|
import { fetch as transport } from "@nsnanocat/util";
|
|
3
3
|
import { $app } from "@nsnanocat/util/lib/app.mjs";
|
|
4
4
|
import { done } from "@nsnanocat/util/lib/done.mjs";
|
|
5
|
+
import { Lodash as _ } from "@nsnanocat/util/polyfill/Lodash.mjs";
|
|
5
6
|
import { Storage } from "@nsnanocat/util/polyfill/Storage";
|
|
6
7
|
import { validatePathParts } from "./lib/settings-path.mjs";
|
|
7
8
|
|
|
8
|
-
const MISSING = Symbol("missing");
|
|
9
|
-
|
|
10
9
|
/**
|
|
11
|
-
* PreferencePanes 后端 API
|
|
12
|
-
* PreferencePanes backend API
|
|
10
|
+
* PreferencePanes 后端 API,只转发模块配置并提供通用持久化操作。
|
|
11
|
+
* PreferencePanes backend API only relaying module configurations and providing generic persistence operations.
|
|
13
12
|
*/
|
|
14
13
|
class API {
|
|
15
14
|
/**
|
|
@@ -31,133 +30,107 @@ class API {
|
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
/**
|
|
34
|
-
*
|
|
35
|
-
* Handle
|
|
33
|
+
* 处理模块配置 API 与固定存储动作,不接管页面或静态资源。
|
|
34
|
+
* Handle module configuration APIs and fixed storage actions without intercepting pages or static assets.
|
|
36
35
|
* @param {import("./index.js").SettingsRequest} request 代理请求 / Proxy request.
|
|
37
36
|
* @returns {Promise<import("./index.js").SettingsResponse | undefined>} API 响应或未接管 / API response or pass-through.
|
|
38
37
|
*/
|
|
39
38
|
async handle(request) {
|
|
40
39
|
const url = new URL(request.url);
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return this.#probe(request, configuration);
|
|
48
|
-
case Boolean(action) && request.method === "POST":
|
|
49
|
-
return this.#action(request, module, action, configuration);
|
|
50
|
-
default:
|
|
51
|
-
return this.#response(request, 405, { error: "Use HEAD for module probes and POST for module actions" });
|
|
52
|
-
}
|
|
40
|
+
const action = /^\/api\/(get|set|delete)\/?$/.exec(url.pathname)?.[1];
|
|
41
|
+
if (action) return this.#store(request, action);
|
|
42
|
+
const module = /^\/api\/([a-zA-Z0-9_-]+)\/?$/.exec(url.pathname)?.[1];
|
|
43
|
+
if (!module) return;
|
|
44
|
+
if (!["HEAD", "GET"].includes(request.method)) return this.#response(request, 405, { error: "Use GET or HEAD for module configuration" });
|
|
45
|
+
return this.#configuration(request, `${url.origin}/configs/${module}`);
|
|
53
46
|
}
|
|
54
47
|
|
|
55
|
-
|
|
48
|
+
/**
|
|
49
|
+
* 从同源业务配置响应模块探测或原始 BoxJS JSON。
|
|
50
|
+
* Respond to a module probe or raw BoxJS JSON from the same-origin business configuration.
|
|
51
|
+
* @param {import("./index.js").SettingsRequest} request 代理请求 / Proxy request.
|
|
52
|
+
* @param {string} configuration 同源配置地址 / Same-origin configuration URL.
|
|
53
|
+
* @returns {Promise<import("./index.js").SettingsResponse>} 配置响应 / Configuration response.
|
|
54
|
+
*/
|
|
55
|
+
async #configuration(request, configuration) {
|
|
56
56
|
let result;
|
|
57
57
|
try {
|
|
58
|
-
result = await transport({ url: configuration, method:
|
|
58
|
+
result = await transport({ url: configuration, method: request.method, timeout: 5000, headers: { Accept: "application/json" } });
|
|
59
59
|
} catch (error) {
|
|
60
60
|
return this.#response(request, 502, { error: error.message });
|
|
61
61
|
}
|
|
62
62
|
const version = this.#header(result.headers, "x-preferencepanes-version");
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
switch (action) {
|
|
70
|
-
case "get": {
|
|
71
|
-
const value = Storage.getItem(payload?.scope ? this.#scopePath(target, payload.scope) : this.#storagePath(target, payload?.key), MISSING);
|
|
72
|
-
return value === MISSING ? this.#response(request, 404, { error: "Stored path does not exist" }) : this.#response(request, 200, value);
|
|
73
|
-
}
|
|
74
|
-
case "set":
|
|
75
|
-
if (!Object.hasOwn(payload ?? {}, "value")) throw Object.assign(new TypeError("A value is required"), { status: 400 });
|
|
76
|
-
if (!Storage.setItem(this.#storagePath(target, payload.key), payload.value)) throw new Error("Storage write failed");
|
|
77
|
-
return this.#response(request, 200, { saved: true });
|
|
78
|
-
case "delete": {
|
|
79
|
-
const path = payload?.scope ? this.#scopePath(target, payload.scope) : this.#storagePath(target, payload?.key);
|
|
80
|
-
if (!Storage.removeItem(path)) throw new Error("Storage write failed");
|
|
81
|
-
return this.#response(request, 200, { deleted: true });
|
|
82
|
-
}
|
|
83
|
-
}
|
|
63
|
+
const contentType = this.#header(result.headers, "content-type") ?? "application/json; charset=utf-8";
|
|
64
|
+
return {
|
|
65
|
+
status: result.statusCode ?? result.status,
|
|
66
|
+
headers: { "Content-Type": contentType, "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff", ...(version ? { "X-PreferencePanes-Version": version } : {}) },
|
|
67
|
+
body: request.method === "HEAD" ? "" : typeof result.body === "string" ? result.body : new TextDecoder().decode(result.body),
|
|
68
|
+
};
|
|
84
69
|
}
|
|
85
70
|
|
|
86
|
-
|
|
87
|
-
|
|
71
|
+
/**
|
|
72
|
+
* 使用唯一 form 字段中的完整 @root.path 执行存储操作。
|
|
73
|
+
* Execute a storage operation using the complete @root.path from the sole form field.
|
|
74
|
+
* @param {import("./index.js").SettingsRequest} request 代理请求 / Proxy request.
|
|
75
|
+
* @param {"get" | "set" | "delete"} action 存储动作 / Storage action.
|
|
76
|
+
* @returns {import("./index.js").SettingsResponse} 操作响应 / Operation response.
|
|
77
|
+
*/
|
|
78
|
+
#store(request, action) {
|
|
79
|
+
const reply = (status, data) => this.#response(request, status, data);
|
|
80
|
+
if (request.method !== "POST") return reply(405, { error: "Use POST with a form body" });
|
|
81
|
+
const headers = Object.fromEntries(Object.entries(request.headers ?? {}).map(([key, value]) => [key.toLowerCase(), value]));
|
|
82
|
+
if (headers["content-type"]?.split(";")[0].trim().toLowerCase() !== "application/x-www-form-urlencoded") return reply(415, { error: "Expected application/x-www-form-urlencoded" });
|
|
83
|
+
if (typeof request.body !== "string" || request.body.length > 65536) return reply(400, { error: "Expected a form body up to 65536 characters" });
|
|
84
|
+
let parts, value;
|
|
88
85
|
try {
|
|
89
|
-
|
|
86
|
+
const fields = request.body.split("&");
|
|
87
|
+
if (fields.length !== 1) throw new TypeError("Send exactly one storage key");
|
|
88
|
+
const separator = fields[0].indexOf("=");
|
|
89
|
+
if (separator < 0) throw new TypeError("Expected @root.path=value");
|
|
90
|
+
const key = decodeURIComponent(fields[0].slice(0, separator).replace(/\+/g, " "));
|
|
91
|
+
value = decodeURIComponent(fields[0].slice(separator + 1).replace(/\+/g, " "));
|
|
92
|
+
if (!key.startsWith("@")) throw new TypeError("Storage keys must start with @");
|
|
93
|
+
parts = validatePathParts(key.slice(1).split("."));
|
|
94
|
+
if (parts.length < 2) throw new TypeError("Specify a storage root and child path");
|
|
90
95
|
} catch (error) {
|
|
91
|
-
|
|
96
|
+
return reply(400, { error: error.message });
|
|
92
97
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const apps = Array.isArray(boxjs) ? [{ settings: boxjs }] : (boxjs.apps ?? [boxjs]);
|
|
99
|
-
if (!Array.isArray(apps)) throw new TypeError("Expected BoxJS apps array");
|
|
100
|
-
const entries = [];
|
|
101
|
-
let storageKey;
|
|
102
|
-
for (const app of apps) {
|
|
103
|
-
if (!app || !Array.isArray(app.settings)) throw new TypeError("Expected BoxJS settings array");
|
|
104
|
-
for (const entry of app.settings) {
|
|
105
|
-
if (typeof entry.id !== "string") throw new TypeError("BoxJS settings require string IDs");
|
|
106
|
-
if (!entry.id.startsWith("@")) {
|
|
107
|
-
if (Array.isArray(boxjs)) throw new TypeError("BoxJS settings require @root.path IDs");
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
const [root, ...parts] = entry.id.slice(1).split(".");
|
|
111
|
-
if (!root || root.startsWith("@") || parts.length < 2) throw new TypeError("A BoxJS setting must be below a literal storage root and module");
|
|
112
|
-
validatePathParts(parts);
|
|
113
|
-
if (parts[0] !== module) continue;
|
|
114
|
-
if (storageKey && storageKey !== root) throw new TypeError(`A module must use one storage root: ${module}`);
|
|
115
|
-
storageKey = root;
|
|
116
|
-
entries.push(entry);
|
|
117
|
-
}
|
|
98
|
+
if (action === "set") {
|
|
99
|
+
try {
|
|
100
|
+
value = JSON.parse(value);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
118
103
|
}
|
|
119
|
-
if (!entries.length) throw new TypeError(`No BoxJS settings for module: ${module}`);
|
|
120
|
-
return { entries, module, storageKey, version: this.#header(result.headers, "x-preferencepanes-version") };
|
|
121
|
-
} catch (error) {
|
|
122
|
-
throw Object.assign(new Error(`Invalid BoxJS: ${error.message}`), { status: 422 });
|
|
123
104
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
#jsonBody(request) {
|
|
127
|
-
const headers = Object.fromEntries(Object.entries(request.headers ?? {}).map(([key, value]) => [key.toLowerCase(), value]));
|
|
128
|
-
if (headers["content-type"]?.split(";")[0].trim().toLowerCase() !== "application/json") throw Object.assign(new TypeError("Expected application/json"), { status: 415 });
|
|
129
|
-
if (typeof request.body !== "string" || request.body.length > 65536) throw Object.assign(new TypeError("Expected a JSON body up to 65536 characters"), { status: 400 });
|
|
105
|
+
const [storageKey, ...path] = parts;
|
|
130
106
|
try {
|
|
131
|
-
|
|
107
|
+
const root = Storage.getItem(storageKey, {});
|
|
108
|
+
if (!isRecord(root)) throw new TypeError("Stored root must be an object");
|
|
109
|
+
const parent = storageParent(root, path, action === "set");
|
|
110
|
+
const key = path.at(-1);
|
|
111
|
+
switch (action) {
|
|
112
|
+
case "get": {
|
|
113
|
+
const result = parent ? _.get(parent, [key]) : undefined;
|
|
114
|
+
return result === undefined ? reply(404, { error: "Stored path does not exist" }) : reply(200, result);
|
|
115
|
+
}
|
|
116
|
+
case "set":
|
|
117
|
+
_.set(parent, [key], value);
|
|
118
|
+
break;
|
|
119
|
+
case "delete":
|
|
120
|
+
if (parent) _.unset(parent, [key]);
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
if (!Storage.setItem(storageKey, root)) throw new Error("Storage write failed");
|
|
124
|
+
return reply(200, action === "set" ? { saved: true } : { deleted: true });
|
|
132
125
|
} catch (error) {
|
|
133
|
-
|
|
126
|
+
return reply(500, { error: error.message });
|
|
134
127
|
}
|
|
135
128
|
}
|
|
136
129
|
|
|
137
|
-
#
|
|
138
|
-
if (typeof key !== "string") throw Object.assign(new TypeError("A BoxJS field path is required"), { status: 400 });
|
|
139
|
-
const path = `@${target.storageKey}.${key}`;
|
|
140
|
-
if (!target.entries.some(entry => entry.id === path)) throw Object.assign(new TypeError(`Unknown BoxJS field: ${key}`), { status: 400 });
|
|
141
|
-
return path;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
#scopePath(target, scope) {
|
|
145
|
-
switch (scope) {
|
|
146
|
-
case "settings":
|
|
147
|
-
return `@${target.storageKey}.${target.module}.Settings`;
|
|
148
|
-
case "caches":
|
|
149
|
-
return `@${target.storageKey}.${target.module}.Caches`;
|
|
150
|
-
case "module":
|
|
151
|
-
return `@${target.storageKey}.${target.module}`;
|
|
152
|
-
default:
|
|
153
|
-
throw Object.assign(new TypeError("Scope must be settings, caches or module"), { status: 400 });
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
#response(request, status, body, extraHeaders = {}) {
|
|
130
|
+
#response(request, status, body) {
|
|
158
131
|
return {
|
|
159
132
|
status,
|
|
160
|
-
headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff"
|
|
133
|
+
headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" },
|
|
161
134
|
body: request.method === "HEAD" ? "" : JSON.stringify(body),
|
|
162
135
|
};
|
|
163
136
|
}
|
|
@@ -168,4 +141,44 @@ class API {
|
|
|
168
141
|
}
|
|
169
142
|
}
|
|
170
143
|
|
|
144
|
+
/**
|
|
145
|
+
* 判断存储根是否为普通对象。
|
|
146
|
+
* Determine whether a storage root is a plain object.
|
|
147
|
+
* @param {unknown} value 待检查值 / Value to inspect.
|
|
148
|
+
* @returns {boolean} 是否为普通对象 / Whether this is a plain object.
|
|
149
|
+
*/
|
|
150
|
+
function isRecord(value) {
|
|
151
|
+
return value !== null && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 遍历父路径,并解码旧存储中的 JSON 字符串中间节点。
|
|
156
|
+
* Traverse parent paths and decode legacy intermediate nodes stored as JSON strings.
|
|
157
|
+
* @param {Record<string, unknown>} root 存储根 / Storage root.
|
|
158
|
+
* @param {string[]} parts 完整路径 / Complete path.
|
|
159
|
+
* @param {boolean} create 是否创建缺失节点 / Whether to create missing parents.
|
|
160
|
+
* @returns {object | undefined} 父节点或 undefined / Parent node or undefined.
|
|
161
|
+
*/
|
|
162
|
+
function storageParent(root, parts, create) {
|
|
163
|
+
let parent = root;
|
|
164
|
+
for (const part of parts.slice(0, -1)) {
|
|
165
|
+
let next = _.get(parent, [part]);
|
|
166
|
+
switch (typeof next) {
|
|
167
|
+
case "undefined":
|
|
168
|
+
if (!create) return;
|
|
169
|
+
next = {};
|
|
170
|
+
break;
|
|
171
|
+
case "string":
|
|
172
|
+
next = JSON.parse(next);
|
|
173
|
+
break;
|
|
174
|
+
default:
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
if (!isRecord(next) && !Array.isArray(next)) throw new TypeError("Stored parent is not an object or array");
|
|
178
|
+
_.set(parent, [part], next);
|
|
179
|
+
parent = next;
|
|
180
|
+
}
|
|
181
|
+
return parent;
|
|
182
|
+
}
|
|
183
|
+
|
|
171
184
|
new API().run();
|
package/src/browser/client.mjs
CHANGED
|
@@ -67,7 +67,7 @@ export class PreferencesClient {
|
|
|
67
67
|
* @returns {Promise<unknown>} Settings 内容或 undefined / Settings content or undefined.
|
|
68
68
|
*/
|
|
69
69
|
async readSettings() {
|
|
70
|
-
const response = await this.#send("get", {
|
|
70
|
+
const response = await this.#send("get", `@${this.#definition.storageKey}.${this.#definition.settingsPath.join(".")}`);
|
|
71
71
|
return response.status === 404 ? undefined : response.json();
|
|
72
72
|
}
|
|
73
73
|
|
|
@@ -77,7 +77,7 @@ export class PreferencesClient {
|
|
|
77
77
|
* @returns {Promise<unknown>} Caches 内容或 undefined / Caches content or undefined.
|
|
78
78
|
*/
|
|
79
79
|
async readCaches() {
|
|
80
|
-
const response = await this.#send("get", {
|
|
80
|
+
const response = await this.#send("get", `@${this.#definition.storageKey}.${this.#module}.Caches`);
|
|
81
81
|
return response.status === 404 ? undefined : response.json();
|
|
82
82
|
}
|
|
83
83
|
|
|
@@ -87,7 +87,7 @@ export class PreferencesClient {
|
|
|
87
87
|
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
88
88
|
*/
|
|
89
89
|
clearCaches() {
|
|
90
|
-
return this.#change("delete", {
|
|
90
|
+
return this.#change("delete", `${this.#module}.Caches`, undefined, "clearCaches");
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/**
|
|
@@ -96,7 +96,7 @@ export class PreferencesClient {
|
|
|
96
96
|
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
97
97
|
*/
|
|
98
98
|
reset() {
|
|
99
|
-
return this.#change("delete",
|
|
99
|
+
return this.#change("delete", this.#module, undefined, "reset");
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
/**
|
|
@@ -116,7 +116,7 @@ export class PreferencesClient {
|
|
|
116
116
|
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
117
117
|
*/
|
|
118
118
|
set(key, value) {
|
|
119
|
-
return this.#change("set",
|
|
119
|
+
return this.#change("set", key, value, "write");
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/**
|
|
@@ -126,30 +126,31 @@ export class PreferencesClient {
|
|
|
126
126
|
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
127
127
|
*/
|
|
128
128
|
remove(key) {
|
|
129
|
-
return this.#change("delete",
|
|
129
|
+
return this.#change("delete", key, undefined, "delete");
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
|
-
*
|
|
134
|
-
* Send a
|
|
135
|
-
* @param {"get" | "set" | "delete"} action
|
|
136
|
-
* @param {
|
|
133
|
+
* 向固定存储 API 发送完整路径的 form 动作。
|
|
134
|
+
* Send a complete-path form action to the fixed storage API.
|
|
135
|
+
* @param {"get" | "set" | "delete"} action 存储动作 / Storage action.
|
|
136
|
+
* @param {string} path 完整 @root.path / Complete @root.path.
|
|
137
|
+
* @param {unknown} [value] set 写入值 / Value written by set.
|
|
137
138
|
* @returns {Promise<Response>} 原始响应 / Raw response.
|
|
138
139
|
*/
|
|
139
|
-
async #send(action,
|
|
140
|
+
async #send(action, path, value) {
|
|
140
141
|
const controller = new AbortController();
|
|
141
142
|
const abort = () => controller.abort();
|
|
142
143
|
if (this.#session.signal.aborted) abort();
|
|
143
144
|
this.#session.signal.addEventListener("abort", abort, { once: true });
|
|
144
145
|
const timer = setTimeout(abort, this.#timeout);
|
|
145
146
|
try {
|
|
146
|
-
const response = await this.#request(`/api/${
|
|
147
|
+
const response = await this.#request(`/api/${action}`, {
|
|
147
148
|
method: "POST",
|
|
148
149
|
credentials: "omit",
|
|
149
150
|
cache: "no-store",
|
|
150
151
|
signal: controller.signal,
|
|
151
|
-
headers: { "Content-Type": "application/
|
|
152
|
-
body: JSON.stringify(
|
|
152
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
153
|
+
body: new URLSearchParams([[path, action === "set" ? JSON.stringify(value) : ""]]).toString(),
|
|
153
154
|
});
|
|
154
155
|
if (response.status !== 200 && !(action === "get" && response.status === 404)) throw new Error(`HTTP ${response.status}`);
|
|
155
156
|
return response;
|
|
@@ -163,28 +164,28 @@ export class PreferencesClient {
|
|
|
163
164
|
* 执行写入动作;成功后只更新当前页面值。
|
|
164
165
|
* Execute a mutation and update only the current page values after success.
|
|
165
166
|
* @param {"set" | "delete"} action API 动作 / API action.
|
|
166
|
-
* @param {
|
|
167
|
+
* @param {string} key 不含存储根的路径 / Path without the storage root.
|
|
168
|
+
* @param {unknown} value set 写入值 / Value written by set.
|
|
167
169
|
* @param {"write" | "delete" | "clearCaches" | "reset"} operation 通知操作 / Notification operation.
|
|
168
|
-
* @param {string} [key] 字段路径 / Field path.
|
|
169
170
|
* @returns {Promise<void>} 操作完成 / Operation completion.
|
|
170
171
|
*/
|
|
171
|
-
async #change(action,
|
|
172
|
+
async #change(action, key, value, operation) {
|
|
172
173
|
if (this.#saving) throw new Error("A settings write is already in progress");
|
|
173
174
|
this.#saving = true;
|
|
174
175
|
try {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
let field;
|
|
177
|
+
if (operation === "write" || operation === "delete") {
|
|
178
|
+
field = this.#definition.fields.find(candidate => candidate.key === key);
|
|
179
|
+
if (!field || (operation === "write" && !validValue(field, value))) throw new TypeError("Invalid setting value");
|
|
178
180
|
}
|
|
179
|
-
await this.#send(action,
|
|
181
|
+
await this.#send(action, `@${this.#definition.storageKey}.${key}`, value);
|
|
180
182
|
switch (operation) {
|
|
181
183
|
case "write":
|
|
182
|
-
this.#values[key] = structuredClone(
|
|
184
|
+
this.#values[key] = structuredClone(value);
|
|
183
185
|
break;
|
|
184
186
|
case "delete": {
|
|
185
|
-
const field = this.#definition.fields.find(candidate => candidate.key === key);
|
|
186
187
|
delete this.#values[key];
|
|
187
|
-
if (
|
|
188
|
+
if (Object.hasOwn(field, "defaultValue")) this.#values[key] = structuredClone(field.defaultValue);
|
|
188
189
|
break;
|
|
189
190
|
}
|
|
190
191
|
case "clearCaches":
|
package/src/browser/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeBoxJs } from "./boxjs.mjs";
|
|
1
2
|
import { statusView } from "./components.mjs";
|
|
2
3
|
import { mount } from "./mount.mjs";
|
|
3
4
|
import { installDefaultStyles } from "./styles.mjs";
|
|
@@ -24,8 +25,8 @@ class ModulePage {
|
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
|
-
*
|
|
28
|
-
* Read BoxJS JSON
|
|
28
|
+
* 通过模块 API 读取 BoxJS JSON 并挂载通用前端。
|
|
29
|
+
* Read BoxJS JSON through the module API and mount the generic frontend.
|
|
29
30
|
* @returns {Promise<void>} 启动完成 / Startup completion.
|
|
30
31
|
*/
|
|
31
32
|
async start() {
|
|
@@ -37,9 +38,11 @@ class ModulePage {
|
|
|
37
38
|
const match = /^\/settings\/([a-zA-Z0-9_-]+)\/?$/.exec(this.#window.location.pathname);
|
|
38
39
|
const module = embedded ?? match?.[1];
|
|
39
40
|
if (!module) throw new TypeError("Open a concrete module URL");
|
|
40
|
-
const response = await fetch(`/
|
|
41
|
+
const response = await fetch(`/api/${encodeURIComponent(module)}`, { cache: "no-store", credentials: "omit", headers: { Accept: "application/json" } });
|
|
41
42
|
if (response.status !== 200) throw new Error(`HTTP ${response.status}`);
|
|
42
|
-
|
|
43
|
+
const boxjs = await response.json();
|
|
44
|
+
normalizeBoxJs(boxjs, module);
|
|
45
|
+
this.#view = mount(boxjs);
|
|
43
46
|
} catch (error) {
|
|
44
47
|
this.#root.replaceChildren(statusView(`加载失败:${error.message}`, () => this.start()));
|
|
45
48
|
}
|
package/src/index.d.ts
CHANGED