@nsnanocat/preference-panes 1.1.1 → 1.1.3
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 +16 -15
- package/dist/api.js +92 -606
- package/dist/module/index.html +1 -1
- package/dist/module/index.mjs +125 -121
- package/dist/module/navigation.mjs +1 -1
- package/dist/preference-panes.mjs +26 -24
- package/dist/web.js +1 -1
- package/package.json +1 -1
- package/src/api.mjs +95 -114
- package/src/browser/ModuleStatus.mjs +1 -1
- package/src/browser/boxjs.mjs +1 -0
- 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
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { URL } from "@nsnanocat/url";
|
|
2
|
-
import { fetch as transport } from "@nsnanocat/util";
|
|
3
2
|
import { $app } from "@nsnanocat/util/lib/app.mjs";
|
|
4
3
|
import { done } from "@nsnanocat/util/lib/done.mjs";
|
|
4
|
+
import { Lodash as _ } from "@nsnanocat/util/polyfill/Lodash.mjs";
|
|
5
5
|
import { Storage } from "@nsnanocat/util/polyfill/Storage";
|
|
6
6
|
import { validatePathParts } from "./lib/settings-path.mjs";
|
|
7
7
|
|
|
8
|
-
const MISSING = Symbol("missing");
|
|
9
|
-
|
|
10
8
|
/**
|
|
11
|
-
* PreferencePanes 后端 API
|
|
12
|
-
* PreferencePanes backend API
|
|
9
|
+
* PreferencePanes 后端 API,只提供通用持久化操作。
|
|
10
|
+
* PreferencePanes backend API providing generic persistence operations only.
|
|
13
11
|
*/
|
|
14
12
|
class API {
|
|
15
13
|
/**
|
|
@@ -31,141 +29,124 @@ class API {
|
|
|
31
29
|
}
|
|
32
30
|
|
|
33
31
|
/**
|
|
34
|
-
*
|
|
35
|
-
* Handle
|
|
32
|
+
* 处理固定存储动作,不接管模块配置、页面或静态资源。
|
|
33
|
+
* Handle fixed storage actions without intercepting module configurations, pages, or static assets.
|
|
36
34
|
* @param {import("./index.js").SettingsRequest} request 代理请求 / Proxy request.
|
|
37
35
|
* @returns {Promise<import("./index.js").SettingsResponse | undefined>} API 响应或未接管 / API response or pass-through.
|
|
38
36
|
*/
|
|
39
37
|
async handle(request) {
|
|
40
38
|
const url = new URL(request.url);
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
const configuration = `${url.origin}/configs/${module}`;
|
|
45
|
-
switch (true) {
|
|
46
|
-
case !action && request.method === "HEAD":
|
|
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
|
-
}
|
|
39
|
+
const action = /^\/api\/(get|set|delete)$/.exec(url.pathname)?.[1];
|
|
40
|
+
if (action) return this.#store(request, action);
|
|
41
|
+
return;
|
|
53
42
|
}
|
|
54
43
|
|
|
55
|
-
|
|
56
|
-
|
|
44
|
+
/**
|
|
45
|
+
* 使用唯一 form 字段中的完整 @root.path 执行存储操作。
|
|
46
|
+
* Execute a storage operation using the complete @root.path from the sole form field.
|
|
47
|
+
* @param {import("./index.js").SettingsRequest} request 代理请求 / Proxy request.
|
|
48
|
+
* @param {"get" | "set" | "delete"} action 存储动作 / Storage action.
|
|
49
|
+
* @returns {import("./index.js").SettingsResponse} 操作响应 / Operation response.
|
|
50
|
+
*/
|
|
51
|
+
#store(request, action) {
|
|
52
|
+
const reply = (status, data) => this.#response(request, status, data);
|
|
53
|
+
if (request.method !== "POST") return reply(405, { error: "Use POST with a form body" });
|
|
54
|
+
const headers = Object.fromEntries(Object.entries(request.headers ?? {}).map(([key, value]) => [key.toLowerCase(), value]));
|
|
55
|
+
if (headers["content-type"]?.split(";")[0].trim().toLowerCase() !== "application/x-www-form-urlencoded") return reply(415, { error: "Expected application/x-www-form-urlencoded" });
|
|
56
|
+
if (typeof request.body !== "string" || request.body.length > 65536) return reply(400, { error: "Expected a form body up to 65536 characters" });
|
|
57
|
+
let parts, value;
|
|
57
58
|
try {
|
|
58
|
-
|
|
59
|
+
const fields = request.body.split("&");
|
|
60
|
+
if (fields.length !== 1) throw new TypeError("Send exactly one storage key");
|
|
61
|
+
const separator = fields[0].indexOf("=");
|
|
62
|
+
if (separator < 0) throw new TypeError("Expected @root.path=value");
|
|
63
|
+
const key = decodeURIComponent(fields[0].slice(0, separator).replace(/\+/g, " "));
|
|
64
|
+
value = decodeURIComponent(fields[0].slice(separator + 1).replace(/\+/g, " "));
|
|
65
|
+
if (!key.startsWith("@")) throw new TypeError("Storage keys must start with @");
|
|
66
|
+
parts = validatePathParts(key.slice(1).split("."));
|
|
67
|
+
if (parts.length < 2) throw new TypeError("Specify a storage root and child path");
|
|
59
68
|
} catch (error) {
|
|
60
|
-
return
|
|
69
|
+
return reply(400, { error: error.message });
|
|
61
70
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const payload = this.#jsonBody(request);
|
|
68
|
-
const target = await this.#load(module, configuration);
|
|
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 });
|
|
71
|
+
if (action === "set") {
|
|
72
|
+
try {
|
|
73
|
+
value = JSON.parse(value);
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (!(error instanceof SyntaxError)) throw error;
|
|
82
76
|
}
|
|
83
77
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
async #load(module, configuration) {
|
|
87
|
-
let result;
|
|
78
|
+
const [storageKey, ...path] = parts;
|
|
88
79
|
try {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const boxjs = JSON.parse(body);
|
|
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);
|
|
80
|
+
const root = Storage.getItem(storageKey, {});
|
|
81
|
+
if (!isRecord(root)) throw new TypeError("Stored root must be an object");
|
|
82
|
+
const parent = storageParent(root, path, action === "set");
|
|
83
|
+
const key = path.at(-1);
|
|
84
|
+
switch (action) {
|
|
85
|
+
case "get": {
|
|
86
|
+
const result = parent ? _.get(parent, [key]) : undefined;
|
|
87
|
+
return result === undefined ? reply(404, { error: "Stored path does not exist" }) : reply(200, result);
|
|
117
88
|
}
|
|
89
|
+
case "set":
|
|
90
|
+
_.set(parent, [key], value);
|
|
91
|
+
break;
|
|
92
|
+
case "delete":
|
|
93
|
+
if (parent) _.unset(parent, [key]);
|
|
94
|
+
break;
|
|
118
95
|
}
|
|
119
|
-
if (!
|
|
120
|
-
return {
|
|
121
|
-
} catch (error) {
|
|
122
|
-
throw Object.assign(new Error(`Invalid BoxJS: ${error.message}`), { status: 422 });
|
|
123
|
-
}
|
|
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 });
|
|
130
|
-
try {
|
|
131
|
-
return JSON.parse(request.body);
|
|
96
|
+
if (!Storage.setItem(storageKey, root)) throw new Error("Storage write failed");
|
|
97
|
+
return reply(200, action === "set" ? { saved: true } : { deleted: true });
|
|
132
98
|
} catch (error) {
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
#storagePath(target, key) {
|
|
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 });
|
|
99
|
+
return reply(500, { error: error.message });
|
|
154
100
|
}
|
|
155
101
|
}
|
|
156
102
|
|
|
157
|
-
#response(request, status, body
|
|
103
|
+
#response(request, status, body) {
|
|
158
104
|
return {
|
|
159
105
|
status,
|
|
160
|
-
headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff"
|
|
106
|
+
headers: { "Content-Type": "application/json; charset=utf-8", "Cache-Control": "no-store", "X-Content-Type-Options": "nosniff" },
|
|
161
107
|
body: request.method === "HEAD" ? "" : JSON.stringify(body),
|
|
162
108
|
};
|
|
163
109
|
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 判断存储根是否为普通对象。
|
|
114
|
+
* Determine whether a storage root is a plain object.
|
|
115
|
+
* @param {unknown} value 待检查值 / Value to inspect.
|
|
116
|
+
* @returns {boolean} 是否为普通对象 / Whether this is a plain object.
|
|
117
|
+
*/
|
|
118
|
+
function isRecord(value) {
|
|
119
|
+
return value !== null && typeof value === "object" && Object.getPrototypeOf(value) === Object.prototype;
|
|
120
|
+
}
|
|
164
121
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
122
|
+
/**
|
|
123
|
+
* 遍历父路径,并解码旧存储中的 JSON 字符串中间节点。
|
|
124
|
+
* Traverse parent paths and decode legacy intermediate nodes stored as JSON strings.
|
|
125
|
+
* @param {Record<string, unknown>} root 存储根 / Storage root.
|
|
126
|
+
* @param {string[]} parts 完整路径 / Complete path.
|
|
127
|
+
* @param {boolean} create 是否创建缺失节点 / Whether to create missing parents.
|
|
128
|
+
* @returns {object | undefined} 父节点或 undefined / Parent node or undefined.
|
|
129
|
+
*/
|
|
130
|
+
function storageParent(root, parts, create) {
|
|
131
|
+
let parent = root;
|
|
132
|
+
for (const part of parts.slice(0, -1)) {
|
|
133
|
+
let next = _.get(parent, [part]);
|
|
134
|
+
switch (typeof next) {
|
|
135
|
+
case "undefined":
|
|
136
|
+
if (!create) return;
|
|
137
|
+
next = {};
|
|
138
|
+
break;
|
|
139
|
+
case "string":
|
|
140
|
+
next = JSON.parse(next);
|
|
141
|
+
break;
|
|
142
|
+
default:
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
if (!isRecord(next) && !Array.isArray(next)) throw new TypeError("Stored parent is not an object or array");
|
|
146
|
+
_.set(parent, [part], next);
|
|
147
|
+
parent = next;
|
|
168
148
|
}
|
|
149
|
+
return parent;
|
|
169
150
|
}
|
|
170
151
|
|
|
171
152
|
new API().run();
|
|
@@ -76,7 +76,7 @@ export class ModuleStatus extends EventTarget {
|
|
|
76
76
|
const response = await probeModule(url, { ...options, signal: controller.signal });
|
|
77
77
|
if (controller !== this.#controller) return response;
|
|
78
78
|
const version = response.status === 200 ? response.headers.get("X-PreferencePanes-Version")?.trim() || null : null;
|
|
79
|
-
this.#render(
|
|
79
|
+
this.#render(version ? "installed" : "missing", version);
|
|
80
80
|
return response;
|
|
81
81
|
} catch (error) {
|
|
82
82
|
if (controller !== this.#controller) return;
|
package/src/browser/boxjs.mjs
CHANGED
|
@@ -25,6 +25,7 @@ export function normalizeBoxJs(config, module) {
|
|
|
25
25
|
if (!storageKey || storageKey.startsWith("@") || parts.length < 2) throw new TypeError("A BoxJS setting must be below a literal storage root and module");
|
|
26
26
|
validatePathParts(parts);
|
|
27
27
|
const name = parts[0];
|
|
28
|
+
if (["get", "set", "delete"].includes(name)) throw new TypeError(`Reserved API module name: ${name}`);
|
|
28
29
|
let target = modules.get(name);
|
|
29
30
|
if (!target) {
|
|
30
31
|
target = { module: name, storageKey, entries: [], owners: new Set() };
|
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