@dsh-plus/web-cache-headers 0.1.3 → 0.1.5
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/lib/client.js +696 -0
- package/package.json +20 -4
- package/src/client/card.tsx +106 -0
- package/src/client/client.ts +50 -0
- package/src/client/i18n.ts +42 -0
- package/src/client/styles.ts +20 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@dsh-plus/web-cache-headers",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
let react = require("react");
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
10
|
+
//#region ../shared/src/client/card.tsx
|
|
11
|
+
const IDLE_STATUS = {
|
|
12
|
+
kind: "idle",
|
|
13
|
+
text: ""
|
|
14
|
+
};
|
|
15
|
+
/** 状态行 + 动作按钮(两种形态共用,类名随形态切换度量)。 */
|
|
16
|
+
function CardFoot(props) {
|
|
17
|
+
const p = props.prefix;
|
|
18
|
+
const section = props.variant === "section";
|
|
19
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
20
|
+
className: section ? `${p}-sectionFooter` : `${p}-footer`,
|
|
21
|
+
children: [props.status !== void 0 && props.status.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
22
|
+
className: `${p}-status${props.status.kind === "error" ? ` ${p}-statusError` : ""}`,
|
|
23
|
+
role: "status",
|
|
24
|
+
children: props.status.text
|
|
25
|
+
}) : null, props.actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
26
|
+
type: "button",
|
|
27
|
+
className: `${p}-btn ${action.variant === "primary" ? `${p}-btnPrimary` : `${p}-btnGhost`}`,
|
|
28
|
+
disabled: action.disabled === true,
|
|
29
|
+
onClick: action.onClick,
|
|
30
|
+
children: action.label
|
|
31
|
+
}, action.key))]
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** 只读提示 + 字段区 + 页脚(两种形态共用)。 */
|
|
35
|
+
function CardBody(props) {
|
|
36
|
+
const p = props.prefix;
|
|
37
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
38
|
+
props.readOnlyNotice !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
39
|
+
className: `${p}-readOnly`,
|
|
40
|
+
role: "status",
|
|
41
|
+
children: props.readOnlyNotice
|
|
42
|
+
}) : null,
|
|
43
|
+
props.children,
|
|
44
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardFoot, { ...props })
|
|
45
|
+
] });
|
|
46
|
+
}
|
|
47
|
+
function CardChrome(props) {
|
|
48
|
+
const p = props.prefix;
|
|
49
|
+
const badge = props.statusBadge;
|
|
50
|
+
if (props.variant === "section") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
51
|
+
className: `${p}-section`,
|
|
52
|
+
"aria-label": props.title,
|
|
53
|
+
children: [
|
|
54
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
55
|
+
className: `${p}-sectionHead`,
|
|
56
|
+
children: [
|
|
57
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h3", {
|
|
58
|
+
className: `${p}-sectionTitle`,
|
|
59
|
+
children: props.title
|
|
60
|
+
}),
|
|
61
|
+
badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
62
|
+
className: `${p}-sectionBadge ${badge.on ? `${p}-statusOn` : `${p}-statusOff`}`,
|
|
63
|
+
children: badge.text
|
|
64
|
+
}) : null,
|
|
65
|
+
props.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
66
|
+
className: `${p}-pending`,
|
|
67
|
+
children: props.dirtyLabel
|
|
68
|
+
}) : null
|
|
69
|
+
]
|
|
70
|
+
}),
|
|
71
|
+
props.description === "" ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
72
|
+
className: `${p}-sectionDesc`,
|
|
73
|
+
children: props.description
|
|
74
|
+
}),
|
|
75
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
76
|
+
className: `${p}-sectionBody`,
|
|
77
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBody, { ...props })
|
|
78
|
+
})
|
|
79
|
+
]
|
|
80
|
+
});
|
|
81
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
|
|
82
|
+
className: `${p}-card${props.open ? ` ${p}-cardOpen` : ""}`,
|
|
83
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
84
|
+
type: "button",
|
|
85
|
+
className: `${p}-header`,
|
|
86
|
+
"aria-expanded": props.open,
|
|
87
|
+
"aria-label": props.title,
|
|
88
|
+
onClick: () => props.onToggle(!props.open),
|
|
89
|
+
children: [
|
|
90
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
91
|
+
className: `${p}-headText`,
|
|
92
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
93
|
+
className: `${p}-name`,
|
|
94
|
+
children: props.title
|
|
95
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
96
|
+
className: `${p}-description`,
|
|
97
|
+
children: props.description
|
|
98
|
+
})]
|
|
99
|
+
}),
|
|
100
|
+
badge !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
101
|
+
className: `${p}-statusBadge ${badge.on ? `${p}-statusOn` : `${p}-statusOff`}`,
|
|
102
|
+
children: badge.text
|
|
103
|
+
}) : null,
|
|
104
|
+
props.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
105
|
+
className: `${p}-pending`,
|
|
106
|
+
children: props.dirtyLabel
|
|
107
|
+
}) : null,
|
|
108
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronDownOutlineRegular, { className: `${p}-chevron${props.open ? ` ${p}-chevronOpen` : ""}` })
|
|
109
|
+
]
|
|
110
|
+
}), props.open ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
111
|
+
className: `${p}-body`,
|
|
112
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardBody, { ...props })
|
|
113
|
+
}) : null]
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
/** 取值未到达时的占位:与 CardChrome 同形态,避免宿主页里闪一次卡片外壳。 */
|
|
117
|
+
function CardLoading(props) {
|
|
118
|
+
if (props.variant === "section") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
119
|
+
className: `${props.prefix}-section`,
|
|
120
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
121
|
+
className: `${props.prefix}-readOnly`,
|
|
122
|
+
role: "status",
|
|
123
|
+
children: props.text
|
|
124
|
+
})
|
|
125
|
+
});
|
|
126
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
127
|
+
className: `${props.prefix}-card`,
|
|
128
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
129
|
+
className: `${props.prefix}-readOnly`,
|
|
130
|
+
role: "status",
|
|
131
|
+
children: props.text
|
|
132
|
+
})
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region ../shared/src/client/config-slots.ts
|
|
137
|
+
/** dsh-plus 聚合 bundle 的包名(各插件 row 均经其补丁层插入 profile)。 */
|
|
138
|
+
const DSH_PLUS_BUNDLE = "@dsh-plus/bundle-main";
|
|
139
|
+
/**
|
|
140
|
+
* 视图 → 外壳形态:插件页 page 视图渲染在官方页面容器内(宿主已画标题、图标、
|
|
141
|
+
* 面包屑与页面级内边距),再套卡片会形成卡片套卡片并压缩移动端可用宽度,
|
|
142
|
+
* 故用无边框分节;summary 与 legacy 槽位保持卡片形态。
|
|
143
|
+
*/
|
|
144
|
+
function cardVariantFor(view) {
|
|
145
|
+
return view === "page" ? "section" : "card";
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* 把一张配置卡片同时注册进 legacy 与新版三个槽位。
|
|
149
|
+
* 任一槽位在当前 dsh 版本未声明时对应 inject 挂起等待,版本不命中即 no-op。
|
|
150
|
+
*/
|
|
151
|
+
function injectPluginConfigCard(slots, reg) {
|
|
152
|
+
const base = {
|
|
153
|
+
locale: reg.ns,
|
|
154
|
+
inject: reg.inject
|
|
155
|
+
};
|
|
156
|
+
slots.inject("settings.plugin.item", () => slots.register({
|
|
157
|
+
name: "settings.plugin.item",
|
|
158
|
+
key: reg.ns,
|
|
159
|
+
...base
|
|
160
|
+
}, reg.component));
|
|
161
|
+
slots.inject("plugins.row.config", () => slots.register({
|
|
162
|
+
name: "plugins.row.config",
|
|
163
|
+
key: `${DSH_PLUS_BUNDLE}#${reg.rowId}`,
|
|
164
|
+
...base
|
|
165
|
+
}, reg.component));
|
|
166
|
+
slots.inject("plugins.bundle.config", () => slots.register({
|
|
167
|
+
name: "plugins.bundle.config",
|
|
168
|
+
key: reg.pkg,
|
|
169
|
+
...base
|
|
170
|
+
}, reg.component));
|
|
171
|
+
}
|
|
172
|
+
//#endregion
|
|
173
|
+
//#region ../shared/src/client/draft.ts
|
|
174
|
+
/**
|
|
175
|
+
* 单命名空间 staged 草稿(简单配置卡片共用):读 scope 快照、播种草稿、
|
|
176
|
+
* 脏判定、保存(revision fencing + 重新装载)与状态行。
|
|
177
|
+
*
|
|
178
|
+
* 语义与官方 staged 表单一致:Host 更新不覆盖在途编辑,只有保存才写回;
|
|
179
|
+
* 保存成功后按 Host 解析值重置草稿。`from`/`patch` 参与 effect 与 useMemo
|
|
180
|
+
* 依赖,必须是模块级稳定函数(不要内联箭头函数)。
|
|
181
|
+
* @module @dsh-plus/shared/client/draft
|
|
182
|
+
*/
|
|
183
|
+
function useNamespaceDraft(options) {
|
|
184
|
+
const { scope, api, from, patch, saveFailedLabel } = options;
|
|
185
|
+
const snapshot = (0, react.useSyncExternalStore)((listener) => scope.subscribe(listener), () => scope.getSnapshot());
|
|
186
|
+
const value = snapshot.value;
|
|
187
|
+
const [draft, setDraftState] = (0, react.useState)(null);
|
|
188
|
+
const [saving, setSaving] = (0, react.useState)(false);
|
|
189
|
+
const [status, setStatus] = (0, react.useState)(IDLE_STATUS);
|
|
190
|
+
(0, react.useEffect)(() => {
|
|
191
|
+
if (value === void 0 || draft !== null) return;
|
|
192
|
+
setDraftState(from(value));
|
|
193
|
+
}, [
|
|
194
|
+
value,
|
|
195
|
+
draft,
|
|
196
|
+
from
|
|
197
|
+
]);
|
|
198
|
+
const dirty = (0, react.useMemo)(() => value !== void 0 && draft !== null && JSON.stringify(patch(draft)) !== JSON.stringify(patch(from(value))), [
|
|
199
|
+
value,
|
|
200
|
+
draft,
|
|
201
|
+
from,
|
|
202
|
+
patch
|
|
203
|
+
]);
|
|
204
|
+
const setDraft = (next) => {
|
|
205
|
+
setDraftState(next);
|
|
206
|
+
setStatus(IDLE_STATUS);
|
|
207
|
+
};
|
|
208
|
+
const edit = (part) => {
|
|
209
|
+
if (draft === null) return;
|
|
210
|
+
setDraft({
|
|
211
|
+
...draft,
|
|
212
|
+
...part
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
const reset = () => {
|
|
216
|
+
if (value === void 0) return;
|
|
217
|
+
setDraft(from(value));
|
|
218
|
+
};
|
|
219
|
+
const save = () => {
|
|
220
|
+
if (draft === null) return;
|
|
221
|
+
setSaving(true);
|
|
222
|
+
const revision = scope.getSnapshot().revision;
|
|
223
|
+
api.update(patch(draft), revision).then(async () => {
|
|
224
|
+
await scope.load();
|
|
225
|
+
const next = scope.getSnapshot().value;
|
|
226
|
+
if (next !== void 0) setDraftState(from(next));
|
|
227
|
+
setStatus(IDLE_STATUS);
|
|
228
|
+
}).catch((error) => {
|
|
229
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
230
|
+
setStatus({
|
|
231
|
+
kind: "error",
|
|
232
|
+
text: `${saveFailedLabel}${message}`
|
|
233
|
+
});
|
|
234
|
+
}).finally(() => setSaving(false));
|
|
235
|
+
};
|
|
236
|
+
return {
|
|
237
|
+
value,
|
|
238
|
+
draft,
|
|
239
|
+
dirty,
|
|
240
|
+
saving,
|
|
241
|
+
status,
|
|
242
|
+
disabled: !snapshot.writable,
|
|
243
|
+
setDraft,
|
|
244
|
+
edit,
|
|
245
|
+
reset,
|
|
246
|
+
save
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region ../shared/src/client/fields.tsx
|
|
251
|
+
/**
|
|
252
|
+
* 勾选行:自绘圆角扁平开关(switch),对齐官方 WebUI 无原生 checkbox 的
|
|
253
|
+
* 设计语言(官方选择类控件全走胶囊/菜单,见 cardCss 的 -switch 规则)。
|
|
254
|
+
* 语义保持 checkbox(无障碍树与表单语义不变),视觉自绘。
|
|
255
|
+
*/
|
|
256
|
+
function CheckRow(props) {
|
|
257
|
+
const p = props.prefix ?? "dshp";
|
|
258
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
259
|
+
className: `${p}-checkRow`,
|
|
260
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
261
|
+
id: props.id,
|
|
262
|
+
type: "checkbox",
|
|
263
|
+
role: "switch",
|
|
264
|
+
"aria-checked": props.checked,
|
|
265
|
+
checked: props.checked,
|
|
266
|
+
disabled: props.disabled === true,
|
|
267
|
+
onChange: (event) => props.onEdit(event.target.checked)
|
|
268
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
269
|
+
htmlFor: props.id,
|
|
270
|
+
children: props.label
|
|
271
|
+
})]
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
//#endregion
|
|
275
|
+
//#region ../shared/src/client/i18n.ts
|
|
276
|
+
/** 卡片通用文案(save/discard/unsaved/loading 等四插件共有键)。 */
|
|
277
|
+
const commonZh = {
|
|
278
|
+
save: "保存",
|
|
279
|
+
saving: "保存中…",
|
|
280
|
+
discard: "放弃",
|
|
281
|
+
unsaved: "未保存",
|
|
282
|
+
loading: "加载中…",
|
|
283
|
+
unavailable: "配置服务不可用。",
|
|
284
|
+
readOnly: "当前部署无 settings provider,配置为只读;请编辑 settings.yaml。",
|
|
285
|
+
invalidNumber: "请输入正整数。",
|
|
286
|
+
collapse: "收起",
|
|
287
|
+
expand: "展开",
|
|
288
|
+
enabledOn: "已启用",
|
|
289
|
+
enabledOff: "未启用",
|
|
290
|
+
saveFailed: "保存失败,请检查填写内容。",
|
|
291
|
+
confirm: "确认",
|
|
292
|
+
cancel: "取消"
|
|
293
|
+
};
|
|
294
|
+
const commonEn = {
|
|
295
|
+
save: "Save",
|
|
296
|
+
saving: "Saving…",
|
|
297
|
+
discard: "Discard",
|
|
298
|
+
unsaved: "Unsaved",
|
|
299
|
+
loading: "Loading…",
|
|
300
|
+
unavailable: "Settings service unavailable.",
|
|
301
|
+
readOnly: "No settings provider in this deployment; config is read-only. Edit settings.yaml.",
|
|
302
|
+
invalidNumber: "Enter a positive integer.",
|
|
303
|
+
collapse: "Collapse",
|
|
304
|
+
expand: "Expand",
|
|
305
|
+
enabledOn: "On",
|
|
306
|
+
enabledOff: "Off",
|
|
307
|
+
saveFailed: "Save failed; check the fields.",
|
|
308
|
+
confirm: "Confirm",
|
|
309
|
+
cancel: "Cancel"
|
|
310
|
+
};
|
|
311
|
+
/**
|
|
312
|
+
* own 覆盖同键合并(返回新对象,入参不可变)。
|
|
313
|
+
* 返回类型保留两侧键集合,调用方据此派生 DictKey。
|
|
314
|
+
*/
|
|
315
|
+
function mergeDict(base, own) {
|
|
316
|
+
return {
|
|
317
|
+
...base,
|
|
318
|
+
...own
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
//#endregion
|
|
322
|
+
//#region ../shared/src/client/scope.ts
|
|
323
|
+
/**
|
|
324
|
+
* remote.settings 直连实现的命名空间 scope(官方配置页 tab 同款模式)。
|
|
325
|
+
* 读:describe 取命名空间视图(脱敏 value/revision + 顶层 writable);
|
|
326
|
+
* 刷新:remote `settings/document-updated`(按命名空间过滤)与
|
|
327
|
+
* `connection/reset`;generation 防旧读覆盖新发布。任何页面 origin
|
|
328
|
+
* (loopback 或 tailnet 信任域名)下行为一致。
|
|
329
|
+
* effect 归属调用方 fiber(label 需含插件名以便诊断)。
|
|
330
|
+
*/
|
|
331
|
+
function createSettingsScope(c, ns, label) {
|
|
332
|
+
const remote = c.get("remote");
|
|
333
|
+
let state = {
|
|
334
|
+
status: "loading",
|
|
335
|
+
value: void 0,
|
|
336
|
+
revision: void 0,
|
|
337
|
+
writable: false
|
|
338
|
+
};
|
|
339
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
340
|
+
let generation = 0;
|
|
341
|
+
const publish = (next) => {
|
|
342
|
+
state = next;
|
|
343
|
+
for (const listener of [...listeners]) listener();
|
|
344
|
+
};
|
|
345
|
+
const load = async () => {
|
|
346
|
+
const gen = ++generation;
|
|
347
|
+
let view;
|
|
348
|
+
try {
|
|
349
|
+
const response = await remote.settings.describe();
|
|
350
|
+
if (!response.ok) return;
|
|
351
|
+
view = response.value;
|
|
352
|
+
} catch {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (gen !== generation) return;
|
|
356
|
+
const namespace = view.namespaces.find((candidate) => candidate.ns === ns);
|
|
357
|
+
if (namespace === void 0) {
|
|
358
|
+
publish({
|
|
359
|
+
status: "unavailable",
|
|
360
|
+
value: void 0,
|
|
361
|
+
revision: void 0,
|
|
362
|
+
writable: view.writable
|
|
363
|
+
});
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
publish({
|
|
367
|
+
status: "ready",
|
|
368
|
+
value: namespace.value,
|
|
369
|
+
revision: namespace.revision,
|
|
370
|
+
writable: view.writable
|
|
371
|
+
});
|
|
372
|
+
};
|
|
373
|
+
const refresh = (namespace) => {
|
|
374
|
+
if (namespace !== void 0 && namespace !== ns) return;
|
|
375
|
+
load();
|
|
376
|
+
};
|
|
377
|
+
const disposers = [remote.$on("settings/document-updated", refresh), c.on("connection/reset", () => {
|
|
378
|
+
load();
|
|
379
|
+
})];
|
|
380
|
+
c.effect(() => () => {
|
|
381
|
+
for (const dispose of disposers) dispose();
|
|
382
|
+
}, label);
|
|
383
|
+
load();
|
|
384
|
+
return {
|
|
385
|
+
getSnapshot: () => state,
|
|
386
|
+
subscribe: (listener) => {
|
|
387
|
+
listeners.add(listener);
|
|
388
|
+
return () => {
|
|
389
|
+
listeners.delete(listener);
|
|
390
|
+
};
|
|
391
|
+
},
|
|
392
|
+
load
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/** 以 remote.settings 构造命名空间绑定的 api 面。 */
|
|
396
|
+
function createNamespaceApi(remote, ns) {
|
|
397
|
+
return {
|
|
398
|
+
update: async (patch, expectedRevision) => {
|
|
399
|
+
const response = await remote.update(ns, patch, expectedRevision);
|
|
400
|
+
if (!response.ok) throw new Error(response.error.message ?? "settings update failed");
|
|
401
|
+
},
|
|
402
|
+
describe: async () => {
|
|
403
|
+
const response = await remote.describe();
|
|
404
|
+
if (!response.ok) throw new Error(response.error.message ?? "settings describe failed");
|
|
405
|
+
return response.value;
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
//#endregion
|
|
410
|
+
//#region ../shared/src/client/styles.ts
|
|
411
|
+
/**
|
|
412
|
+
* 配置卡片样式工厂(各插件 client 的公共收编版)。
|
|
413
|
+
* 视觉规则与迁移前逐字一致(类名前缀换为各插件自身,DOM 类名不变),
|
|
414
|
+
* 另补移动端与可达性增强(767px 断点 + pointer:coarse,对齐 ui-mobile-fit 约定):
|
|
415
|
+
* - 窄屏 input/textarea 16px(防 iOS 聚焦缩放)、可点目标 ≥44px;
|
|
416
|
+
* - footer 按钮等宽换行、状态文字独占一行;
|
|
417
|
+
* - 展开态 body 底部 sticky 保存栏(长表单滚动中始终可达)。
|
|
418
|
+
* - 分节形态(-section,插件页 page 视图内嵌宿主页面):无边框/背景、横向零
|
|
419
|
+
* 内边距,页脚不 sticky——官方插件页已提供页面级内边距,再叠一层卡片会
|
|
420
|
+
* 缩窄移动端可用宽度(卡片套卡片)。
|
|
421
|
+
* 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载),不覆盖上游选择器。
|
|
422
|
+
* @module @dsh-plus/shared/client/styles
|
|
423
|
+
*/
|
|
424
|
+
/** 生成全套卡片 CSS;prefix 为插件类名前缀(如 'dne'),extra 为插件特有规则。 */
|
|
425
|
+
function cardCss(prefix, extra = "") {
|
|
426
|
+
return `
|
|
427
|
+
.${prefix}-card{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:12px;list-style:none;transition:border-color .16s,background .16s}
|
|
428
|
+
.${prefix}-card:hover{border-color:var(--dsw-alias-label-dimmed)}
|
|
429
|
+
.${prefix}-cardOpen{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
|
|
430
|
+
.${prefix}-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
|
|
431
|
+
.${prefix}-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
|
|
432
|
+
.${prefix}-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
|
|
433
|
+
.${prefix}-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
|
|
434
|
+
.${prefix}-description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
|
|
435
|
+
.${prefix}-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s;display:inline-flex}
|
|
436
|
+
.${prefix}-chevronOpen{transform:rotate(180deg)}
|
|
437
|
+
.${prefix}-pending{white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
438
|
+
.${prefix}-statusBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
439
|
+
.${prefix}-statusOn{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
|
|
440
|
+
.${prefix}-statusOff{color:var(--dsw-alias-label-tertiary)}
|
|
441
|
+
.${prefix}-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-bottom:8px}
|
|
442
|
+
.${prefix}-field{flex-direction:column;gap:6px;padding:12px 0;display:flex}
|
|
443
|
+
.${prefix}-field+.${prefix}-field{border-top:1px solid var(--dsw-alias-border-l2)}
|
|
444
|
+
.${prefix}-head{align-items:center;gap:8px;display:flex}
|
|
445
|
+
.${prefix}-label{min-width:0;color:var(--dsw-alias-label-primary);flex:1;font-size:13px;font-weight:500;line-height:1.5}
|
|
446
|
+
.${prefix}-badge{white-space:nowrap;border-radius:999px;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
447
|
+
.${prefix}-badgeSet{background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
|
|
448
|
+
.${prefix}-badgeUnset{color:var(--dsw-alias-label-tertiary)}
|
|
449
|
+
.${prefix}-input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);height:34px;font:inherit;color:var(--dsw-alias-label-primary);border-radius:8px;padding:0 12px;font-size:13px;transition:border-color .18s,background .18s}
|
|
450
|
+
.${prefix}-input:hover:not(:disabled):not(:focus-visible){border-color:var(--dsw-alias-label-dimmed)}
|
|
451
|
+
.${prefix}-input:focus-visible{border-color:var(--dsw-alias-brand-primary);outline:none}
|
|
452
|
+
.${prefix}-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
|
|
453
|
+
.${prefix}-inputInvalid{border-color:var(--dsw-alias-label-error)}
|
|
454
|
+
.${prefix}-select{appearance:none;background:var(--dsw-alias-bg-module-platform);border:none;cursor:pointer;background-image:linear-gradient(45deg,transparent 50%,var(--dsw-alias-label-secondary) 50%),linear-gradient(135deg,var(--dsw-alias-label-secondary) 50%,transparent 50%);background-position:calc(100% - 16px) 50%,calc(100% - 11px) 50%;background-size:5px 5px;background-repeat:no-repeat;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;height:34px;box-sizing:border-box;border-radius:8px;padding:0 30px 0 12px;transition:background-color .18s}
|
|
455
|
+
.${prefix}-select:hover:not(:disabled){background-color:var(--dsw-alias-interactive-bg-hover)}
|
|
456
|
+
.${prefix}-hint{color:var(--dsw-alias-label-tertiary);margin:0;font-size:12px;line-height:1.5}
|
|
457
|
+
.${prefix}-invalid{color:var(--dsw-alias-label-error);margin:0;font-size:12px;line-height:1.5}
|
|
458
|
+
.${prefix}-checkRow{align-items:center;gap:10px;display:flex;padding:6px 0}
|
|
459
|
+
.${prefix}-checkRow label{color:var(--dsw-alias-label-primary);font-size:13px;line-height:1.5;cursor:pointer;flex:1;min-width:0}
|
|
460
|
+
/* 自绘圆角扁平开关(官方 UI 无可见原生 checkbox,选择类全走胶囊/菜单) */
|
|
461
|
+
.${prefix}-checkRow input{appearance:none;-webkit-appearance:none;position:relative;box-sizing:border-box;background:var(--dsw-alias-bg-module-platform);border:none;border-radius:999px;width:34px;height:20px;flex:none;cursor:pointer;transition:background .18s;margin:0}
|
|
462
|
+
.${prefix}-checkRow input::after{content:"";position:absolute;top:2px;left:2px;background:var(--dsw-alias-label-primary);border-radius:999px;width:16px;height:16px;transition:left .18s cubic-bezier(.4,0,.2,1)}
|
|
463
|
+
.${prefix}-checkRow input:checked{background:var(--dsw-alias-brand-primary)}
|
|
464
|
+
.${prefix}-checkRow input:checked::after{left:16px;background:var(--dsw-alias-bg-base)}
|
|
465
|
+
.${prefix}-checkRow input:disabled{opacity:.4;cursor:default}
|
|
466
|
+
.${prefix}-checkRow input:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:2px}
|
|
467
|
+
/* 宿主内嵌分节形态(插件页 page 视图):官方设置页同构——无边框/背景,
|
|
468
|
+
横向零内边距(宽度交给宿主页面容器),小节标题 13px/600,页脚非 sticky。 */
|
|
469
|
+
.${prefix}-section{box-sizing:border-box;flex-direction:column;width:100%;min-width:0;padding:16px 0;display:flex}
|
|
470
|
+
.${prefix}-sectionHead{align-items:center;gap:8px;min-width:0;display:flex}
|
|
471
|
+
.${prefix}-sectionTitle{min-width:0;color:var(--dsw-alias-label-primary);flex:1;margin:0;font-size:13px;font-weight:600;line-height:1.5}
|
|
472
|
+
.${prefix}-sectionBadge{white-space:nowrap;border-radius:999px;flex:none;padding:1px 8px;font-size:11px;font-weight:500;line-height:17px}
|
|
473
|
+
.${prefix}-sectionDesc{color:var(--dsw-alias-label-tertiary);margin:4px 0 0;font-size:12px;line-height:1.5}
|
|
474
|
+
.${prefix}-sectionBody{flex-direction:column;min-width:0;display:flex}
|
|
475
|
+
.${prefix}-sectionFooter{align-items:center;gap:8px;padding-top:16px;display:flex;flex-wrap:wrap}
|
|
476
|
+
.${prefix}-groupLabel{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:600;padding:12px 0 2px;margin:0}
|
|
477
|
+
.${prefix}-readOnly{color:var(--dsw-alias-label-tertiary);margin:12px 0 0;font-size:12px;line-height:1.5}
|
|
478
|
+
.${prefix}-warn{color:var(--dsw-alias-label-error);margin:12px 0 0;font-size:12px;line-height:1.5}
|
|
479
|
+
.${prefix}-footer{border-top:1px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex;flex-wrap:wrap}
|
|
480
|
+
.${prefix}-status{min-width:0;color:var(--dsw-alias-label-secondary);flex:1;margin:0;font-size:12px;line-height:1.5}
|
|
481
|
+
.${prefix}-statusError{color:var(--dsw-alias-label-error)}
|
|
482
|
+
.${prefix}-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid #0000;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}
|
|
483
|
+
.${prefix}-btnGhost{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}
|
|
484
|
+
.${prefix}-btnGhost:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
|
|
485
|
+
.${prefix}-btnPrimary{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
|
|
486
|
+
.${prefix}-btn:disabled{opacity:.4;cursor:default}
|
|
487
|
+
.${prefix}-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
|
|
488
|
+
@media (max-width:767px){
|
|
489
|
+
.${prefix}-body{margin:0 10px}
|
|
490
|
+
.${prefix}-header{padding:12px 12px}
|
|
491
|
+
.${prefix}-input,.${prefix}-select{font-size:16px;height:40px}
|
|
492
|
+
.${prefix}-checkRow{min-height:44px;padding:8px 0}
|
|
493
|
+
.${prefix}-footer{position:sticky;bottom:0;background:var(--dsw-alias-bg-layer-2);margin:0 -10px;padding:10px 10px 6px;border-top:1px solid var(--dsw-alias-border-l2);border-bottom-left-radius:12px;border-bottom-right-radius:12px}
|
|
494
|
+
.${prefix}-status{flex-basis:100%;flex:none}
|
|
495
|
+
.${prefix}-btn{min-height:44px;flex:1;min-width:96px}
|
|
496
|
+
.${prefix}-statusBadge{display:none}
|
|
497
|
+
.${prefix}-sectionBadge{display:inline-block}
|
|
498
|
+
.${prefix}-sectionFooter{position:static}
|
|
499
|
+
}
|
|
500
|
+
@media (pointer:coarse){
|
|
501
|
+
.${prefix}-btn{min-height:44px}
|
|
502
|
+
.${prefix}-checkRow{min-height:44px}
|
|
503
|
+
.${prefix}-input,.${prefix}-select{height:40px}
|
|
504
|
+
}
|
|
505
|
+
${extra}
|
|
506
|
+
`;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* 幂等注入样式标签(官方 data-plugin-css 约定);返回新标签,
|
|
510
|
+
* 已存在或环境无 document 时返回 null(调用方 effect 卸载用)。
|
|
511
|
+
*/
|
|
512
|
+
function injectCardStyle(pluginId, css) {
|
|
513
|
+
if (typeof document === "undefined") return null;
|
|
514
|
+
const tagId = `${pluginId}/card.css`;
|
|
515
|
+
if (document.querySelector(`style[data-plugin-css=${JSON.stringify(tagId)}]`) !== null) return null;
|
|
516
|
+
const tag = document.createElement("style");
|
|
517
|
+
tag.dataset.plugin = pluginId;
|
|
518
|
+
tag.dataset.pluginCss = tagId;
|
|
519
|
+
tag.textContent = css;
|
|
520
|
+
document.head.appendChild(tag);
|
|
521
|
+
return tag;
|
|
522
|
+
}
|
|
523
|
+
//#endregion
|
|
524
|
+
//#region src/ns.ts
|
|
525
|
+
/**
|
|
526
|
+
* settings 命名空间字面量(纯常量,零依赖)。
|
|
527
|
+
* 服务端以字面量注册 settings section(用户层配置落 $DSH_HOME/settings.yaml,
|
|
528
|
+
* 热生效);与行级 cordis Config 共用同一份 schemastery schema(config.ts)。
|
|
529
|
+
* @module @dsh-plus/web-cache-headers/ns
|
|
530
|
+
*/
|
|
531
|
+
const SETTINGS_NS = "dsh-plus-web-cache-headers";
|
|
532
|
+
//#endregion
|
|
533
|
+
//#region src/client/card.tsx
|
|
534
|
+
/**
|
|
535
|
+
* 「静态资源长缓存」配置卡片:经 injectPluginConfigCard 注册(legacy
|
|
536
|
+
* settings.plugin.item 与插件页 plugins.row.config / plugins.bundle.config,
|
|
537
|
+
* view 分发 summary/page)。
|
|
538
|
+
* 只有 enabled 一个字段:关闭即卸下 writeHead 补丁;范围说明写进卡片
|
|
539
|
+
* (哪些路径不在范围内),避免用户误以为能缓存 index.html。
|
|
540
|
+
* 外壳与控件走 @dsh-plus/shared/client 套件(CardChrome/CheckRow/useNamespaceDraft)。
|
|
541
|
+
* @module web-cache-headers/client/card
|
|
542
|
+
*/
|
|
543
|
+
/** 模块级稳定函数:参与 useNamespaceDraft 的依赖比较。 */
|
|
544
|
+
function draftFromValue(value) {
|
|
545
|
+
return { enabled: value.enabled };
|
|
546
|
+
}
|
|
547
|
+
function toPatch(draft) {
|
|
548
|
+
return { enabled: draft.enabled };
|
|
549
|
+
}
|
|
550
|
+
function WebCacheHeadersCard(props) {
|
|
551
|
+
const { t, scope, api } = props;
|
|
552
|
+
const { draft, dirty, saving, status, disabled, edit, reset, save } = useNamespaceDraft({
|
|
553
|
+
scope,
|
|
554
|
+
api,
|
|
555
|
+
from: draftFromValue,
|
|
556
|
+
patch: toPatch,
|
|
557
|
+
saveFailedLabel: t("saveFailed")
|
|
558
|
+
});
|
|
559
|
+
const [open, setOpen] = (0, react.useState)(props.view === "page");
|
|
560
|
+
if (props.view === "summary") return t("summaryLine");
|
|
561
|
+
const variant = cardVariantFor(props.view);
|
|
562
|
+
if (draft === null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(CardLoading, {
|
|
563
|
+
prefix: "wch",
|
|
564
|
+
variant,
|
|
565
|
+
text: t("loading")
|
|
566
|
+
});
|
|
567
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(CardChrome, {
|
|
568
|
+
prefix: "wch",
|
|
569
|
+
title: t("title"),
|
|
570
|
+
description: t("description"),
|
|
571
|
+
open,
|
|
572
|
+
onToggle: setOpen,
|
|
573
|
+
variant,
|
|
574
|
+
statusBadge: {
|
|
575
|
+
text: t(draft.enabled ? "enabledOn" : "enabledOff"),
|
|
576
|
+
on: draft.enabled
|
|
577
|
+
},
|
|
578
|
+
dirty,
|
|
579
|
+
dirtyLabel: t("unsaved"),
|
|
580
|
+
readOnlyNotice: disabled ? t("readOnly") : void 0,
|
|
581
|
+
status,
|
|
582
|
+
actions: [{
|
|
583
|
+
key: "discard",
|
|
584
|
+
label: t("discard"),
|
|
585
|
+
disabled: !dirty || saving,
|
|
586
|
+
onClick: reset
|
|
587
|
+
}, {
|
|
588
|
+
key: "save",
|
|
589
|
+
label: t(saving ? "saving" : "save"),
|
|
590
|
+
variant: "primary",
|
|
591
|
+
disabled: !dirty || saving || disabled,
|
|
592
|
+
onClick: save
|
|
593
|
+
}],
|
|
594
|
+
children: [
|
|
595
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(CheckRow, {
|
|
596
|
+
prefix: "wch",
|
|
597
|
+
id: "wch-enabled",
|
|
598
|
+
label: t("enabled"),
|
|
599
|
+
checked: draft.enabled,
|
|
600
|
+
disabled,
|
|
601
|
+
onEdit: (v) => edit({ enabled: v })
|
|
602
|
+
}),
|
|
603
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
604
|
+
className: "wch-hint",
|
|
605
|
+
children: t("enabledHint")
|
|
606
|
+
}),
|
|
607
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
608
|
+
className: "wch-scope",
|
|
609
|
+
children: t("scopeHint")
|
|
610
|
+
})
|
|
611
|
+
]
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
//#endregion
|
|
615
|
+
//#region src/client/i18n.ts
|
|
616
|
+
/**
|
|
617
|
+
* 配置卡片文案(zh/en)。经 ctx.locale.register 注册、bind 取用。
|
|
618
|
+
* 公共键(save/discard/unsaved 等)来自 shared 的 common 字典,本文件只维护业务键。
|
|
619
|
+
* @module web-cache-headers/client/i18n
|
|
620
|
+
*/
|
|
621
|
+
const NS = "dsh-plus-web-cache-headers";
|
|
622
|
+
const ownZh = {
|
|
623
|
+
title: "静态资源长缓存",
|
|
624
|
+
description: "为内容哈希命名的 /assets/* 响应补 Cache-Control: immutable,消除每次刷新重复传输约 1.4MB JS/CSS。",
|
|
625
|
+
summaryLine: "给内容寻址的 /assets/* 补 immutable 缓存头,刷新不再全量重下。",
|
|
626
|
+
enabled: "启用长缓存补丁",
|
|
627
|
+
enabledHint: "关闭即卸下补丁,响应头恢复 dsh 原生行为(热生效,无需重启)。development 环境恒禁用:保护 dev/HMR 的同 URL 热更新语义。",
|
|
628
|
+
scopeHint: "只影响 /assets/*(dist 内容哈希资源)。index.html、favicon、manifest 与 /plugins/* 不在范围内:前者保持每次最新,后者官方已自带 immutable。"
|
|
629
|
+
};
|
|
630
|
+
const ownEn = {
|
|
631
|
+
title: "Static asset caching",
|
|
632
|
+
description: "Adds Cache-Control: immutable to content-hashed /assets/* responses, removing the ~1.4MB JS/CSS re-download on every refresh.",
|
|
633
|
+
summaryLine: "Immutable cache headers for content-addressed /assets/*; no full re-download on refresh.",
|
|
634
|
+
enabled: "Enable immutable-assets patch",
|
|
635
|
+
enabledHint: "Off removes the patch and restores native headers (hot, no restart). Always disabled in development to protect same-URL HMR updates.",
|
|
636
|
+
scopeHint: "Affects /assets/* only (content-hashed dist files). index.html, favicon, manifest and /plugins/* stay out of scope: the first three must stay fresh, the last is already immutable."
|
|
637
|
+
};
|
|
638
|
+
const zh = mergeDict(commonZh, ownZh);
|
|
639
|
+
const en = mergeDict(commonEn, ownEn);
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region src/client/styles.ts
|
|
642
|
+
/**
|
|
643
|
+
* 配置卡片样式:基础规则走 @dsh-plus/shared/client 的 cardCss('wch'),
|
|
644
|
+
* 本文件只补插件特有规则(范围说明段落)。
|
|
645
|
+
* 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载)。
|
|
646
|
+
* @module web-cache-headers/client/styles
|
|
647
|
+
*/
|
|
648
|
+
const PLUGIN_ID = "@dsh-plus/web-cache-headers";
|
|
649
|
+
const cardCssAll = cardCss("wch", `
|
|
650
|
+
.wch-scope{color:var(--dsw-alias-label-tertiary);margin:8px 0 0;font-size:12px;line-height:1.5}
|
|
651
|
+
`);
|
|
652
|
+
/** 幂等注入样式标签;返回标签(已存在或环境无 document 时为 null)。 */
|
|
653
|
+
function injectStyle() {
|
|
654
|
+
return injectCardStyle(PLUGIN_ID, cardCssAll);
|
|
655
|
+
}
|
|
656
|
+
//#endregion
|
|
657
|
+
//#region src/client/client.ts
|
|
658
|
+
const name = "dsh-plus-web-cache-headers";
|
|
659
|
+
/** 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject 管包加载顺序)。 */
|
|
660
|
+
const inject = [
|
|
661
|
+
"slots",
|
|
662
|
+
"locale",
|
|
663
|
+
"remote",
|
|
664
|
+
"remote.settings"
|
|
665
|
+
];
|
|
666
|
+
function apply(ctx) {
|
|
667
|
+
const c = ctx;
|
|
668
|
+
const tag = injectStyle();
|
|
669
|
+
c.effect(() => () => {
|
|
670
|
+
tag?.remove();
|
|
671
|
+
}, "web-cache-headers: style");
|
|
672
|
+
c.effect(() => c.locale.register(NS, {
|
|
673
|
+
zh,
|
|
674
|
+
en
|
|
675
|
+
}), "web-cache-headers: locale");
|
|
676
|
+
const scope = createSettingsScope(c, SETTINGS_NS, "web-cache-headers: settings scope");
|
|
677
|
+
const api = createNamespaceApi(c.get("remote").settings, SETTINGS_NS);
|
|
678
|
+
injectPluginConfigCard(c.slots, {
|
|
679
|
+
ns: SETTINGS_NS,
|
|
680
|
+
rowId: "dsh-plus-web-cache-headers",
|
|
681
|
+
pkg: "@dsh-plus/web-cache-headers",
|
|
682
|
+
component: WebCacheHeadersCard,
|
|
683
|
+
inject: () => ({
|
|
684
|
+
t: c.locale.bind(NS),
|
|
685
|
+
scope,
|
|
686
|
+
api
|
|
687
|
+
})
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
//#endregion
|
|
691
|
+
exports.apply = apply;
|
|
692
|
+
exports.inject = inject;
|
|
693
|
+
exports.name = name;
|
|
694
|
+
return module.exports;
|
|
695
|
+
}
|
|
696
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dsh-plus/web-cache-headers",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "dsh-plus service plugin: GUI 静态资源长缓存——为内容哈希的 /assets/* 响应补 `Cache-Control: immutable`(/plugins 官方已自带),消除每次刷新重复传输 ~1.4MB JS/CSS;进程级 writeHead 增量拦截,零路由注册、零 dsh 服务耦合,已有缓存头优先保证未来共存",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -10,6 +10,9 @@
|
|
|
10
10
|
"types": "./lib/index.d.ts",
|
|
11
11
|
"default": "./lib/index.js"
|
|
12
12
|
},
|
|
13
|
+
"./client": {
|
|
14
|
+
"default": "./lib/client.js"
|
|
15
|
+
},
|
|
13
16
|
"./src/*": "./src/*",
|
|
14
17
|
"./package.json": "./package.json"
|
|
15
18
|
},
|
|
@@ -17,8 +20,18 @@
|
|
|
17
20
|
"lib",
|
|
18
21
|
"src"
|
|
19
22
|
],
|
|
23
|
+
"dsh": {
|
|
24
|
+
"client": {
|
|
25
|
+
"inject": [
|
|
26
|
+
"@deepseek-ai/dsh-client-locale",
|
|
27
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
28
|
+
"@deepseek-ai/dsh-api-remotes"
|
|
29
|
+
],
|
|
30
|
+
"platform": "web"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
20
33
|
"dependencies": {
|
|
21
|
-
"@dsh-plus/shared": "0.1.
|
|
34
|
+
"@dsh-plus/shared": "0.1.31"
|
|
22
35
|
},
|
|
23
36
|
"peerDependencies": {
|
|
24
37
|
"@deepseek-ai/cordis": "^4.0.4",
|
|
@@ -28,7 +41,9 @@
|
|
|
28
41
|
"devDependencies": {
|
|
29
42
|
"@deepseek-ai/cordis": "4.0.4",
|
|
30
43
|
"@deepseek-ai/dsh-settings": "0.2.0-rc.1",
|
|
31
|
-
"@deepseek-ai/schemastery": "3.18.4"
|
|
44
|
+
"@deepseek-ai/schemastery": "3.18.4",
|
|
45
|
+
"@types/react": "~18.3.31",
|
|
46
|
+
"react": "^18.2.0"
|
|
32
47
|
},
|
|
33
48
|
"publishConfig": {
|
|
34
49
|
"access": "public"
|
|
@@ -40,6 +55,7 @@
|
|
|
40
55
|
"directory": "packages/web-cache-headers"
|
|
41
56
|
},
|
|
42
57
|
"scripts": {
|
|
43
|
-
"build": "tsdown
|
|
58
|
+
"build": "tsdown",
|
|
59
|
+
"watch": "tsdown --watch"
|
|
44
60
|
}
|
|
45
61
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 「静态资源长缓存」配置卡片:经 injectPluginConfigCard 注册(legacy
|
|
3
|
+
* settings.plugin.item 与插件页 plugins.row.config / plugins.bundle.config,
|
|
4
|
+
* view 分发 summary/page)。
|
|
5
|
+
* 只有 enabled 一个字段:关闭即卸下 writeHead 补丁;范围说明写进卡片
|
|
6
|
+
* (哪些路径不在范围内),避免用户误以为能缓存 index.html。
|
|
7
|
+
* 外壳与控件走 @dsh-plus/shared/client 套件(CardChrome/CheckRow/useNamespaceDraft)。
|
|
8
|
+
* @module web-cache-headers/client/card
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
CardChrome,
|
|
13
|
+
CardLoading,
|
|
14
|
+
CheckRow,
|
|
15
|
+
cardVariantFor,
|
|
16
|
+
type NamespaceSettingsApi,
|
|
17
|
+
type PluginConfigViewProps,
|
|
18
|
+
type Scope,
|
|
19
|
+
useNamespaceDraft,
|
|
20
|
+
} from '@dsh-plus/shared/client'
|
|
21
|
+
import { type ReactElement, useState } from 'react'
|
|
22
|
+
|
|
23
|
+
import type { Translate } from './i18n.ts'
|
|
24
|
+
|
|
25
|
+
export interface CardProps extends PluginConfigViewProps {
|
|
26
|
+
t: Translate
|
|
27
|
+
scope: Scope
|
|
28
|
+
api: NamespaceSettingsApi
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** settings 命名空间的解析值。 */
|
|
32
|
+
export interface ConfigValue {
|
|
33
|
+
enabled: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface Draft {
|
|
37
|
+
enabled: boolean
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 模块级稳定函数:参与 useNamespaceDraft 的依赖比较。 */
|
|
41
|
+
function draftFromValue(value: ConfigValue): Draft {
|
|
42
|
+
return { enabled: value.enabled }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toPatch(draft: Draft): Record<string, unknown> {
|
|
46
|
+
return { enabled: draft.enabled }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function WebCacheHeadersCard(props: CardProps): ReactElement | string | null {
|
|
50
|
+
const { t, scope, api } = props
|
|
51
|
+
const { draft, dirty, saving, status, disabled, edit, reset, save } = useNamespaceDraft<
|
|
52
|
+
ConfigValue,
|
|
53
|
+
Draft
|
|
54
|
+
>({ scope, api, from: draftFromValue, patch: toPatch, saveFailedLabel: t('saveFailed') })
|
|
55
|
+
// 插件页 page 视图为表单落地页,默认展开;旧槽位无 view,保持折叠。
|
|
56
|
+
const [open, setOpen] = useState(props.view === 'page')
|
|
57
|
+
|
|
58
|
+
// 插件页 summary 视图只出一行简介(hooks 已全部落定,可安全提前返回)。
|
|
59
|
+
if (props.view === 'summary') return t('summaryLine')
|
|
60
|
+
|
|
61
|
+
// 插件页 page 视图内嵌宿主页面容器(已带页面级内边距与标题),用无边框分节。
|
|
62
|
+
const variant = cardVariantFor(props.view)
|
|
63
|
+
if (draft === null) return <CardLoading prefix="wch" variant={variant} text={t('loading')} />
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<CardChrome
|
|
67
|
+
prefix="wch"
|
|
68
|
+
title={t('title')}
|
|
69
|
+
description={t('description')}
|
|
70
|
+
open={open}
|
|
71
|
+
onToggle={setOpen}
|
|
72
|
+
variant={variant}
|
|
73
|
+
statusBadge={{ text: t(draft.enabled ? 'enabledOn' : 'enabledOff'), on: draft.enabled }}
|
|
74
|
+
dirty={dirty}
|
|
75
|
+
dirtyLabel={t('unsaved')}
|
|
76
|
+
readOnlyNotice={disabled ? t('readOnly') : undefined}
|
|
77
|
+
status={status}
|
|
78
|
+
actions={[
|
|
79
|
+
{
|
|
80
|
+
key: 'discard',
|
|
81
|
+
label: t('discard'),
|
|
82
|
+
disabled: !dirty || saving,
|
|
83
|
+
onClick: reset,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
key: 'save',
|
|
87
|
+
label: t(saving ? 'saving' : 'save'),
|
|
88
|
+
variant: 'primary',
|
|
89
|
+
disabled: !dirty || saving || disabled,
|
|
90
|
+
onClick: save,
|
|
91
|
+
},
|
|
92
|
+
]}
|
|
93
|
+
>
|
|
94
|
+
<CheckRow
|
|
95
|
+
prefix="wch"
|
|
96
|
+
id="wch-enabled"
|
|
97
|
+
label={t('enabled')}
|
|
98
|
+
checked={draft.enabled}
|
|
99
|
+
disabled={disabled}
|
|
100
|
+
onEdit={(v) => edit({ enabled: v })}
|
|
101
|
+
/>
|
|
102
|
+
<p className="wch-hint">{t('enabledHint')}</p>
|
|
103
|
+
<p className="wch-scope">{t('scopeHint')}</p>
|
|
104
|
+
</CardChrome>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 浏览器半入口:注册 locale 字典 + 注册「静态资源长缓存」卡片
|
|
3
|
+
* (injectPluginConfigCard 三槽位:legacy settings.plugin.item 与插件页
|
|
4
|
+
* plugins.row.config / plugins.bundle.config)。
|
|
5
|
+
* 构建产物为 window.__ModuleLoader__.load({id, factory}) 形式的 CJS factory
|
|
6
|
+
* (包装见 tsdown.config.ts);样式沿用官方 data-plugin-css 约定,HMR 据此卸载。
|
|
7
|
+
* 配置读写经 ctx.remote.settings 直连(scope/api 来自 @dsh-plus/shared/client)。
|
|
8
|
+
* @module web-cache-headers/client
|
|
9
|
+
*/
|
|
10
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
11
|
+
import {
|
|
12
|
+
createNamespaceApi,
|
|
13
|
+
createSettingsScope,
|
|
14
|
+
injectPluginConfigCard,
|
|
15
|
+
type PluginClientContext,
|
|
16
|
+
} from '@dsh-plus/shared/client'
|
|
17
|
+
|
|
18
|
+
import { SETTINGS_NS } from '../ns.ts'
|
|
19
|
+
import { WebCacheHeadersCard } from './card.tsx'
|
|
20
|
+
import { type DictKey, en, NS, zh } from './i18n.ts'
|
|
21
|
+
import { injectStyle } from './styles.ts'
|
|
22
|
+
|
|
23
|
+
export const name = 'dsh-plus-web-cache-headers'
|
|
24
|
+
|
|
25
|
+
/** 浏览器半需要的 cordis 服务 key(loader 据此注入;package.json 的 dsh.client.inject 管包加载顺序)。 */
|
|
26
|
+
export const inject = ['slots', 'locale', 'remote', 'remote.settings'] as const
|
|
27
|
+
|
|
28
|
+
/** 宿主窄面收编在 @dsh-plus/shared/client;TKey 传入本包 DictKey 使键名受检。 */
|
|
29
|
+
type ClientContext = PluginClientContext<DictKey>
|
|
30
|
+
|
|
31
|
+
export function apply(ctx: Context): void {
|
|
32
|
+
const c = ctx as unknown as ClientContext
|
|
33
|
+
const tag = injectStyle()
|
|
34
|
+
c.effect(
|
|
35
|
+
() => () => {
|
|
36
|
+
tag?.remove()
|
|
37
|
+
},
|
|
38
|
+
'web-cache-headers: style',
|
|
39
|
+
)
|
|
40
|
+
c.effect(() => c.locale.register(NS, { zh, en }), 'web-cache-headers: locale')
|
|
41
|
+
const scope = createSettingsScope(c, SETTINGS_NS, 'web-cache-headers: settings scope')
|
|
42
|
+
const api = createNamespaceApi(c.get('remote').settings, SETTINGS_NS)
|
|
43
|
+
injectPluginConfigCard(c.slots, {
|
|
44
|
+
ns: SETTINGS_NS,
|
|
45
|
+
rowId: 'dsh-plus-web-cache-headers',
|
|
46
|
+
pkg: '@dsh-plus/web-cache-headers',
|
|
47
|
+
component: WebCacheHeadersCard,
|
|
48
|
+
inject: () => ({ t: c.locale.bind(NS), scope, api }),
|
|
49
|
+
})
|
|
50
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置卡片文案(zh/en)。经 ctx.locale.register 注册、bind 取用。
|
|
3
|
+
* 公共键(save/discard/unsaved 等)来自 shared 的 common 字典,本文件只维护业务键。
|
|
4
|
+
* @module web-cache-headers/client/i18n
|
|
5
|
+
*/
|
|
6
|
+
import { commonEn, commonZh, mergeDict } from '@dsh-plus/shared/client'
|
|
7
|
+
|
|
8
|
+
export const NS = 'dsh-plus-web-cache-headers'
|
|
9
|
+
|
|
10
|
+
const ownZh = {
|
|
11
|
+
title: '静态资源长缓存',
|
|
12
|
+
description:
|
|
13
|
+
'为内容哈希命名的 /assets/* 响应补 Cache-Control: immutable,消除每次刷新重复传输约 1.4MB JS/CSS。',
|
|
14
|
+
summaryLine: '给内容寻址的 /assets/* 补 immutable 缓存头,刷新不再全量重下。',
|
|
15
|
+
enabled: '启用长缓存补丁',
|
|
16
|
+
enabledHint:
|
|
17
|
+
'关闭即卸下补丁,响应头恢复 dsh 原生行为(热生效,无需重启)。development 环境恒禁用:保护 dev/HMR 的同 URL 热更新语义。',
|
|
18
|
+
scopeHint:
|
|
19
|
+
'只影响 /assets/*(dist 内容哈希资源)。index.html、favicon、manifest 与 /plugins/* 不在范围内:前者保持每次最新,后者官方已自带 immutable。',
|
|
20
|
+
} as const
|
|
21
|
+
|
|
22
|
+
const ownEn = {
|
|
23
|
+
title: 'Static asset caching',
|
|
24
|
+
description:
|
|
25
|
+
'Adds Cache-Control: immutable to content-hashed /assets/* responses, removing the ~1.4MB JS/CSS re-download on every refresh.',
|
|
26
|
+
summaryLine:
|
|
27
|
+
'Immutable cache headers for content-addressed /assets/*; no full re-download on refresh.',
|
|
28
|
+
enabled: 'Enable immutable-assets patch',
|
|
29
|
+
enabledHint:
|
|
30
|
+
'Off removes the patch and restores native headers (hot, no restart). Always disabled in development to protect same-URL HMR updates.',
|
|
31
|
+
scopeHint:
|
|
32
|
+
'Affects /assets/* only (content-hashed dist files). index.html, favicon, manifest and /plugins/* stay out of scope: the first three must stay fresh, the last is already immutable.',
|
|
33
|
+
} as const
|
|
34
|
+
|
|
35
|
+
export type DictKey = keyof typeof commonZh | keyof typeof ownZh
|
|
36
|
+
|
|
37
|
+
/** 卡片共用的翻译函数类型(slot 注入的 bind 结果按此消费)。 */
|
|
38
|
+
export type Translate = (key: DictKey) => string
|
|
39
|
+
|
|
40
|
+
// 标注为 Record<DictKey, string>:en 缺任一键即编译期报错(中英强制对齐)。
|
|
41
|
+
export const zh: Record<DictKey, string> = mergeDict(commonZh, ownZh)
|
|
42
|
+
export const en: Record<DictKey, string> = mergeDict(commonEn, ownEn)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 配置卡片样式:基础规则走 @dsh-plus/shared/client 的 cardCss('wch'),
|
|
3
|
+
* 本文件只补插件特有规则(范围说明段落)。
|
|
4
|
+
* 沿用官方 data-plugin / data-plugin-css 约定(HMR 据此卸载)。
|
|
5
|
+
* @module web-cache-headers/client/styles
|
|
6
|
+
*/
|
|
7
|
+
import { cardCss, injectCardStyle } from '@dsh-plus/shared/client'
|
|
8
|
+
|
|
9
|
+
export const PLUGIN_ID = '@dsh-plus/web-cache-headers'
|
|
10
|
+
|
|
11
|
+
const extraCss = `
|
|
12
|
+
.wch-scope{color:var(--dsw-alias-label-tertiary);margin:8px 0 0;font-size:12px;line-height:1.5}
|
|
13
|
+
`
|
|
14
|
+
|
|
15
|
+
export const cardCssAll = cardCss('wch', extraCss)
|
|
16
|
+
|
|
17
|
+
/** 幂等注入样式标签;返回标签(已存在或环境无 document 时为 null)。 */
|
|
18
|
+
export function injectStyle(): HTMLStyleElement | null {
|
|
19
|
+
return injectCardStyle(PLUGIN_ID, cardCssAll)
|
|
20
|
+
}
|