dsh-plugin-toolkit 0.10.0 → 0.11.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/CHANGELOG.md +16 -0
- package/README.md +3 -0
- package/README.zh.md +2 -0
- package/client.js +96 -14
- package/lib/testkit/load.mjs +7 -1
- package/lib/testkit/mount.mjs +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## 0.11.0
|
|
2
|
+
|
|
3
|
+
- **A language switch, in the panel.** 中文 / EN sits in the dock's header row and
|
|
4
|
+
in the sidebar tab, opens in Chinese (the audience this plugin is written for),
|
|
5
|
+
and remembers the choice in `localStorage` under `dsh-plugin-toolkit:lang`. The
|
|
6
|
+
panel no longer follows the host's language silently: the app can be in English
|
|
7
|
+
while the person reading it is not.
|
|
8
|
+
- `useLang()` is a tiny subscription store, so the dock, the tab and the tab
|
|
9
|
+
type's `title()` all switch together.
|
|
10
|
+
- **The testkit hands a bundle the real `window`.** `loadBundle` used to pass a
|
|
11
|
+
stand-in that carried only `__ModuleLoader__`, so `window.localStorage` was
|
|
12
|
+
`undefined` and any preference silently failed to save. That bug was found by
|
|
13
|
+
this feature failing its own persistence assertion.
|
|
14
|
+
- Fixed invalid markup: the switch was nested inside the dock's toggle button.
|
|
15
|
+
React warns and drops the inner button, so it now sits beside it.
|
|
16
|
+
|
|
1
17
|
## 0.10.0
|
|
2
18
|
|
|
3
19
|
- **The checker is docked under the conversation.** It used to live only in the
|
package/README.md
CHANGED
|
@@ -98,6 +98,9 @@ for, how to use it, what each check means, and what it cannot tell you — plus
|
|
|
98
98
|
the folder field and the results. The same panel is also a **Plugin doctor** tab
|
|
99
99
|
in the right sidebar, for when the conversation is busy.
|
|
100
100
|
|
|
101
|
+
The panel speaks both languages and opens in Chinese; the 中文 / EN switch sits
|
|
102
|
+
in the dock's header row (and in the sidebar tab) and the choice is remembered.
|
|
103
|
+
|
|
101
104
|
## Test a panel without a browser
|
|
102
105
|
|
|
103
106
|
A plugin's client code is one file that calls `window.__ModuleLoader__.load({factory})` — there is no module to import, so the usual React test setup does not apply. `testkit` supplies the missing half: a DOM, the real React, and a host context that records what the panel registers.
|
package/README.zh.md
CHANGED
|
@@ -57,6 +57,8 @@ cd my-panel && npm install && npm test
|
|
|
57
57
|
|
|
58
58
|
检查器固定在**对话栏下方、输入框上方**:默认收起成一行,点开就是完整指南——这是给谁用的、怎么用、每一项检查拦什么、以及它证明不了什么——下面才是文件夹输入框和结果。同一个面板也在右侧栏有一个 **Plugin doctor** 标签页,对话区忙的时候用那个。
|
|
59
59
|
|
|
60
|
+
面板中英双语,默认中文;**中文 / EN** 开关就在对话栏那一行的右侧(右侧栏标签里也有),选过之后会记住。
|
|
61
|
+
|
|
60
62
|
## 不用浏览器测面板
|
|
61
63
|
|
|
62
64
|
```js
|
package/client.js
CHANGED
|
@@ -3,7 +3,7 @@ window.__ModuleLoader__.load({
|
|
|
3
3
|
factory: (require) => {
|
|
4
4
|
const module = { exports: {} };
|
|
5
5
|
const React = require('react');
|
|
6
|
-
const { useState, useCallback } = React;
|
|
6
|
+
const { useState, useCallback, useEffect } = React;
|
|
7
7
|
|
|
8
8
|
const NS = 'dsh-plugin-toolkit';
|
|
9
9
|
const KIND = 'toolkit';
|
|
@@ -132,11 +132,74 @@ window.__ModuleLoader__.load({
|
|
|
132
132
|
* type's `title`/`description` are called by the sidebar's guide list, not
|
|
133
133
|
* by this component, and they need the same strings.
|
|
134
134
|
*/
|
|
135
|
-
const translate = (key) => {
|
|
136
|
-
|
|
137
|
-
return COPY[lang][key] ?? key;
|
|
135
|
+
const translate = (key, lang = readLang()) => {
|
|
136
|
+
return (COPY[lang] ?? COPY.en)[key] ?? key;
|
|
138
137
|
};
|
|
139
138
|
|
|
139
|
+
/**
|
|
140
|
+
* The panel's language is the user's choice, not the host's: the app may be
|
|
141
|
+
* in English while the person reading it is not. Chinese is the default,
|
|
142
|
+
* because that is the audience this plugin is written for, and the choice is
|
|
143
|
+
* remembered per browser.
|
|
144
|
+
*/
|
|
145
|
+
const LANG_KEY = 'dsh-plugin-toolkit:lang';
|
|
146
|
+
const LANGUAGES = ['zh', 'en'];
|
|
147
|
+
const langListeners = new Set();
|
|
148
|
+
let langCache = null;
|
|
149
|
+
|
|
150
|
+
function readLang() {
|
|
151
|
+
if (langCache !== null) return langCache;
|
|
152
|
+
try {
|
|
153
|
+
const stored = window.localStorage.getItem(LANG_KEY);
|
|
154
|
+
if (LANGUAGES.includes(stored)) {
|
|
155
|
+
langCache = stored;
|
|
156
|
+
return langCache;
|
|
157
|
+
}
|
|
158
|
+
} catch {
|
|
159
|
+
// No storage (private mode, a sandboxed frame): fall through to the default.
|
|
160
|
+
}
|
|
161
|
+
langCache = 'zh';
|
|
162
|
+
return langCache;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function setLang(next) {
|
|
166
|
+
if (!LANGUAGES.includes(next)) return;
|
|
167
|
+
langCache = next;
|
|
168
|
+
try {
|
|
169
|
+
window.localStorage.setItem(LANG_KEY, next);
|
|
170
|
+
} catch {
|
|
171
|
+
// Remembering the choice is a convenience, never a requirement.
|
|
172
|
+
}
|
|
173
|
+
for (const listener of langListeners) listener();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function useLang() {
|
|
177
|
+
const [lang, set] = useState(readLang);
|
|
178
|
+
useEffect(() => {
|
|
179
|
+
const listener = () => set(readLang());
|
|
180
|
+
langListeners.add(listener);
|
|
181
|
+
return () => langListeners.delete(listener);
|
|
182
|
+
}, []);
|
|
183
|
+
return lang;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function LangSwitch({ lang }) {
|
|
187
|
+
return h('span', { className: 'tk-lang', role: 'group', 'aria-label': 'Language' },
|
|
188
|
+
LANGUAGES.map((code) => h('button', {
|
|
189
|
+
key: code,
|
|
190
|
+
type: 'button',
|
|
191
|
+
className: 'tk-lang-button',
|
|
192
|
+
'data-on': code === lang ? 'true' : 'false',
|
|
193
|
+
'aria-pressed': code === lang ? 'true' : 'false',
|
|
194
|
+
onClick: (event) => {
|
|
195
|
+
// The dock sits inside the composer: a click here must not reach
|
|
196
|
+
// the conversation's own handlers.
|
|
197
|
+
event.stopPropagation();
|
|
198
|
+
setLang(code);
|
|
199
|
+
},
|
|
200
|
+
}, code === 'zh' ? '中文' : 'EN')));
|
|
201
|
+
}
|
|
202
|
+
|
|
140
203
|
const h = React.createElement;
|
|
141
204
|
|
|
142
205
|
const CSS = [
|
|
@@ -165,6 +228,13 @@ window.__ModuleLoader__.load({
|
|
|
165
228
|
'.tk-check{font-size:11px;text-transform:uppercase;letter-spacing:.04em;',
|
|
166
229
|
'color:var(--dsw-alias-label-tertiary,#888)}',
|
|
167
230
|
'.tk-message{font-size:12px;overflow-wrap:anywhere}',
|
|
231
|
+
'.tk-panel-head{display:flex;align-items:center;justify-content:space-between;gap:8px}',
|
|
232
|
+
'.tk-lang{display:inline-flex;flex:none;border:0.5px solid var(--dsw-alias-border-l2,#ccc);',
|
|
233
|
+
'border-radius:6px;overflow:hidden}',
|
|
234
|
+
'.tk-lang-button{height:22px;padding:0 8px;border:0;background:none;font:inherit;font-size:11px;',
|
|
235
|
+
'color:var(--dsw-alias-label-secondary,#555);cursor:pointer}',
|
|
236
|
+
'.tk-lang-button[data-on="true"]{background:var(--dsw-alias-state-business-primary,#4176e6);color:#fff}',
|
|
237
|
+
'.tk-lang-button:focus-visible{outline:1px solid var(--dsw-alias-state-business-primary,#4176e6);outline-offset:-1px}',
|
|
168
238
|
'.tk-dock{border-top:0.5px solid var(--dsw-alias-border-l2,#e5e5e5);margin:0 12px}',
|
|
169
239
|
'.tk-dock-head{display:flex}',
|
|
170
240
|
'.tk-dock-toggle{display:flex;align-items:center;gap:8px;width:100%;padding:7px 2px;',
|
|
@@ -236,13 +306,15 @@ window.__ModuleLoader__.load({
|
|
|
236
306
|
}
|
|
237
307
|
|
|
238
308
|
function Form({ checker }) {
|
|
309
|
+
const lang = useLang();
|
|
310
|
+
const tr = (key) => translate(key, lang);
|
|
239
311
|
return h('div', { className: 'tk-form' },
|
|
240
312
|
h('input', {
|
|
241
313
|
className: 'tk-input',
|
|
242
314
|
type: 'text',
|
|
243
315
|
value: checker.dir,
|
|
244
|
-
placeholder:
|
|
245
|
-
'aria-label':
|
|
316
|
+
placeholder: tr('placeholder'),
|
|
317
|
+
'aria-label': tr('placeholder'),
|
|
246
318
|
onChange: (event) => checker.setDir(event.target.value),
|
|
247
319
|
onKeyDown: (event) => { if (event.key === 'Enter') checker.check(); },
|
|
248
320
|
}),
|
|
@@ -251,25 +323,31 @@ window.__ModuleLoader__.load({
|
|
|
251
323
|
type: 'button',
|
|
252
324
|
disabled: checker.busy === true,
|
|
253
325
|
onClick: checker.check,
|
|
254
|
-
}, checker.busy === true ?
|
|
326
|
+
}, checker.busy === true ? tr('checking') : tr('check')));
|
|
255
327
|
}
|
|
256
328
|
|
|
257
329
|
function Result({ checker }) {
|
|
330
|
+
const lang = useLang();
|
|
331
|
+
const tr = (key) => translate(key, lang);
|
|
258
332
|
return h('div', { className: 'tk-result' },
|
|
259
333
|
checker.failed !== null ? h('p', { className: 'tk-failed', role: 'status' }, checker.failed) : null,
|
|
260
334
|
checker.result !== null
|
|
261
335
|
? h('div', { className: 'tk-result-body' },
|
|
262
336
|
h('p', { className: 'tk-verdict', 'data-ok': checker.result.ok === true ? 'true' : 'false', role: 'status' },
|
|
263
|
-
checker.result.ok === true ?
|
|
337
|
+
checker.result.ok === true ? tr('pass') : tr('fail')),
|
|
264
338
|
h('ul', { className: 'tk-rows' }, checker.result.checks.map((entry) => h(Row, { key: entry.check, entry }))))
|
|
265
339
|
: null);
|
|
266
340
|
}
|
|
267
341
|
|
|
268
342
|
function Panel() {
|
|
269
343
|
const checker = useChecker();
|
|
344
|
+
const lang = useLang();
|
|
345
|
+
const tr = (key) => translate(key, lang);
|
|
270
346
|
return h('div', { className: 'tk-panel' },
|
|
271
|
-
h('
|
|
272
|
-
|
|
347
|
+
h('div', { className: 'tk-panel-head' },
|
|
348
|
+
h('h2', { className: 'tk-title' }, tr('title')),
|
|
349
|
+
h(LangSwitch, { lang })),
|
|
350
|
+
h('p', { className: 'tk-hint' }, tr('hint')),
|
|
273
351
|
h(Form, { checker }),
|
|
274
352
|
h(Result, { checker }));
|
|
275
353
|
}
|
|
@@ -282,19 +360,23 @@ window.__ModuleLoader__.load({
|
|
|
282
360
|
function Dock() {
|
|
283
361
|
const [open, setOpen] = useState(false);
|
|
284
362
|
const checker = useChecker();
|
|
285
|
-
const
|
|
363
|
+
const lang = useLang();
|
|
364
|
+
const guide = GUIDE[lang] ?? GUIDE.zh;
|
|
286
365
|
|
|
287
366
|
return h('section', { className: 'tk-dock', 'data-open': open === true ? 'true' : 'false' },
|
|
288
367
|
h('div', { className: 'tk-dock-head' },
|
|
368
|
+
// The language switch is a sibling of the toggle, never inside it: a
|
|
369
|
+
// button inside a button is invalid and React drops the inner one.
|
|
289
370
|
h('button', {
|
|
290
371
|
className: 'tk-dock-toggle',
|
|
291
372
|
type: 'button',
|
|
292
373
|
'aria-expanded': open === true ? 'true' : 'false',
|
|
293
374
|
onClick: () => setOpen(open !== true),
|
|
294
375
|
},
|
|
295
|
-
h('span', { className: 'tk-dock-title' }, translate('dockTitle')),
|
|
296
|
-
h('span', { className: 'tk-dock-sub' }, translate('dockSub')),
|
|
297
|
-
h('span', { className: 'tk-dock-chevron', 'aria-hidden': 'true' }, open === true ? '▾' : '▸'))
|
|
376
|
+
h('span', { className: 'tk-dock-title' }, translate('dockTitle', lang)),
|
|
377
|
+
h('span', { className: 'tk-dock-sub' }, translate('dockSub', lang)),
|
|
378
|
+
h('span', { className: 'tk-dock-chevron', 'aria-hidden': 'true' }, open === true ? '▾' : '▸')),
|
|
379
|
+
h(LangSwitch, { lang })),
|
|
298
380
|
open === true
|
|
299
381
|
? h('div', { className: 'tk-dock-body' },
|
|
300
382
|
h('div', { className: 'tk-guide' },
|
package/lib/testkit/load.mjs
CHANGED
|
@@ -46,7 +46,13 @@ export function createModuleLoader({ require: provide, onLoad } = {}) {
|
|
|
46
46
|
export function loadBundle(file, options = {}) {
|
|
47
47
|
const source = readBundleSource(file);
|
|
48
48
|
const loader = createModuleLoader(options);
|
|
49
|
-
|
|
49
|
+
// The bundle's `window` must be the real one: a plugin reads
|
|
50
|
+
// `window.localStorage`, `window.matchMedia`, `window.navigator`, and a
|
|
51
|
+
// stand-in that only carries the loader turns every one of those into a
|
|
52
|
+
// silent `undefined`.
|
|
53
|
+
const hostWindow = options.window ?? {};
|
|
54
|
+
hostWindow.__ModuleLoader__ = loader;
|
|
55
|
+
const context = { window: hostWindow, __ModuleLoader__: loader, ...(options.globals ?? {}) };
|
|
50
56
|
// The bundle is an IIFE or a top-level statement list; the browser would run it
|
|
51
57
|
// as a classic script, so a function body is the closest equivalent.
|
|
52
58
|
const run = new Function('window', '__ModuleLoader__', 'require', 'module', 'exports', source);
|
package/lib/testkit/mount.mjs
CHANGED
|
@@ -74,7 +74,7 @@ export async function mountClient(options) {
|
|
|
74
74
|
const previousFetch = globalThis.fetch;
|
|
75
75
|
globalThis.fetch = host.fetch;
|
|
76
76
|
try {
|
|
77
|
-
const exported = loadBundle(file, { require: moduleShim(modules) });
|
|
77
|
+
const exported = loadBundle(file, { require: moduleShim(modules), window });
|
|
78
78
|
const apply = exported.apply ?? exported.default ?? exported.install ?? exported;
|
|
79
79
|
if (typeof apply !== 'function') {
|
|
80
80
|
throw new Error(`${file} exports no apply(ctx) function, so there is nothing to mount`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-toolkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Preflight, release and test DeepSeek Harness plugins: checks what the host needs, waits for npm to actually serve the version, and keeps the market index honest.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|