dsh-plugin-toolkit 0.9.5 → 0.9.7
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 +32 -0
- package/client.js +23 -5
- package/index.js +13 -5
- package/lib/verify.mjs +37 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
## 0.9.7
|
|
2
|
+
|
|
3
|
+
- **The tab type is now reachable.** Registering a tab *type* is not enough: the
|
|
4
|
+
right sidebar's guide list is built from `tabs.guide()`, so a type with no
|
|
5
|
+
`guide` entry is registered, has a body slot, and is still impossible to open —
|
|
6
|
+
no tab appears anywhere. The type now contributes one guide entry
|
|
7
|
+
(`id: 'doctor'`, `order: 60`, localized title and description), which is what
|
|
8
|
+
puts "Plugin doctor" in the sidebar's guide.
|
|
9
|
+
- The title and description come from the panel's own dictionary through a
|
|
10
|
+
module-level `translate()`, because the sidebar calls them from its guide list,
|
|
11
|
+
outside this component.
|
|
12
|
+
- Regression test: mounting the bundle with a recording `sidebarRightTabs` now
|
|
13
|
+
asserts the registered definition carries a guide entry with a title and an
|
|
14
|
+
order.
|
|
15
|
+
|
|
16
|
+
## 0.9.6
|
|
17
|
+
|
|
18
|
+
- **The host half now survives a context it does not own.** `apply()` ended with
|
|
19
|
+
an unguarded `installRoutes(ctx)`, and cordis does not return `undefined` for
|
|
20
|
+
an undeclared service — it throws `cannot get property "webServer" without
|
|
21
|
+
inject`. So the whole `apply` failed, no route was registered, and the plugin
|
|
22
|
+
manager showed one red dot with no text: the exact symptom the 0.9.5 report
|
|
23
|
+
channel was built to explain, caused by the code next to it.
|
|
24
|
+
- `verify` now calls the host face's `apply` with a context that throws on every
|
|
25
|
+
undeclared property, the way cordis does. A plain `{}` hid this: reading
|
|
26
|
+
`ctx.webServer` off it is just `undefined`. Message:
|
|
27
|
+
`apply() throws when a service it never declared is read, which fails the row: …`
|
|
28
|
+
- The scaffold was never affected — its host half registers nothing.
|
|
29
|
+
- Regression tests: `test/host.test.mjs` (activation, deferred web server,
|
|
30
|
+
method guard) and a `verify` case that builds a plugin whose `apply` reads
|
|
31
|
+
`ctx.webServer` unguarded.
|
|
32
|
+
|
|
1
33
|
## 0.9.5
|
|
2
34
|
|
|
3
35
|
- The panel reports its own failures. When a row fails to activate the app shows
|
package/client.js
CHANGED
|
@@ -20,6 +20,7 @@ window.__ModuleLoader__.load({
|
|
|
20
20
|
fail: 'This would not install cleanly.',
|
|
21
21
|
noDir: 'Name the folder that holds the plugin\'s package.json.',
|
|
22
22
|
failed: 'Could not run the checks',
|
|
23
|
+
guideDescription: 'Run the install checks on a plugin folder',
|
|
23
24
|
},
|
|
24
25
|
zh: {
|
|
25
26
|
tabTitle: '插件体检',
|
|
@@ -32,9 +33,20 @@ window.__ModuleLoader__.load({
|
|
|
32
33
|
fail: '这样装上去会出问题。',
|
|
33
34
|
noDir: '请填写放着 package.json 的插件文件夹。',
|
|
34
35
|
failed: '检查没能跑起来',
|
|
36
|
+
guideDescription: '对任意插件文件夹跑一遍安装前的检查',
|
|
35
37
|
},
|
|
36
38
|
};
|
|
37
39
|
|
|
40
|
+
/**
|
|
41
|
+
* The panel's copy, by the document language. Module scope because the tab
|
|
42
|
+
* type's `title`/`description` are called by the sidebar's guide list, not
|
|
43
|
+
* by this component, and they need the same strings.
|
|
44
|
+
*/
|
|
45
|
+
const translate = (key) => {
|
|
46
|
+
const lang = typeof document === 'undefined' ? 'en' : (document.documentElement.lang === 'zh' ? 'zh' : 'en');
|
|
47
|
+
return COPY[lang][key] ?? key;
|
|
48
|
+
};
|
|
49
|
+
|
|
38
50
|
const h = React.createElement;
|
|
39
51
|
|
|
40
52
|
const CSS = [
|
|
@@ -89,10 +101,7 @@ window.__ModuleLoader__.load({
|
|
|
89
101
|
const [result, setResult] = useState(null);
|
|
90
102
|
const [busy, setBusy] = useState(false);
|
|
91
103
|
const [failed, setFailed] = useState(null);
|
|
92
|
-
const tr =
|
|
93
|
-
const language = COPY[document.documentElement.lang === 'zh' ? 'zh' : 'en'];
|
|
94
|
-
return language[key] ?? key;
|
|
95
|
-
};
|
|
104
|
+
const tr = translate;
|
|
96
105
|
|
|
97
106
|
const check = useCallback(() => {
|
|
98
107
|
if (dir.trim().length === 0) {
|
|
@@ -209,11 +218,20 @@ window.__ModuleLoader__.load({
|
|
|
209
218
|
key: NS,
|
|
210
219
|
locale: NS,
|
|
211
220
|
}, Panel)));
|
|
221
|
+
// A tab *type* with no guide entry is registered but unreachable:
|
|
222
|
+
// the sidebar's guide list is built from `tabs.guide()`, so the
|
|
223
|
+
// type would never be offered and nothing would open it.
|
|
212
224
|
tabCtx.effect(() => tabCtx.sidebarRightTabs.register({
|
|
213
225
|
id: NS,
|
|
214
226
|
kind: KIND,
|
|
215
227
|
priority: 'extension',
|
|
216
|
-
title: () => '
|
|
228
|
+
title: () => translate('tabTitle'),
|
|
229
|
+
guide: [{
|
|
230
|
+
id: 'doctor',
|
|
231
|
+
order: 60,
|
|
232
|
+
title: () => translate('tabTitle'),
|
|
233
|
+
description: () => translate('guideDescription'),
|
|
234
|
+
}],
|
|
217
235
|
}));
|
|
218
236
|
report('tab-registered', `kind=${KIND}`);
|
|
219
237
|
} catch (error) {
|
package/index.js
CHANGED
|
@@ -143,11 +143,19 @@ export function apply(ctx) {
|
|
|
143
143
|
let installed = false;
|
|
144
144
|
const installRoutes = (serviceCtx) => {
|
|
145
145
|
if (installed) return;
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
146
|
+
// Reading `ctx.webServer` on a context that does not declare `inject` does
|
|
147
|
+
// not yield undefined: cordis throws "cannot get property ... without
|
|
148
|
+
// inject". An unguarded read here fails the whole apply, which is exactly
|
|
149
|
+
// the red dot with no message this panel exists to explain.
|
|
150
|
+
try {
|
|
151
|
+
const server = serviceCtx !== null && serviceCtx !== undefined ? serviceCtx.webServer : undefined;
|
|
152
|
+
if (server === null || server === undefined || typeof server.register !== 'function') return;
|
|
153
|
+
server.register({ kind: 'exact', path: '/toolkit/check', handler: checkHandler });
|
|
154
|
+
server.register({ kind: 'exact', path: '/toolkit/report', handler: reportHandler });
|
|
155
|
+
installed = true;
|
|
156
|
+
} catch {
|
|
157
|
+
// No web server on this context: the guard still runs, the panel does not.
|
|
158
|
+
}
|
|
151
159
|
};
|
|
152
160
|
|
|
153
161
|
const defer = (deps, callback) => {
|
package/lib/verify.mjs
CHANGED
|
@@ -35,6 +35,21 @@ function packTarball(dir, destination) {
|
|
|
35
35
|
return path.join(destination, document.filename);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* What cordis hands a plugin that declares no `inject`: every undeclared
|
|
40
|
+
* property read throws. A plain `{}` hides the bug this is looking for.
|
|
41
|
+
*/
|
|
42
|
+
function undeclaredContext() {
|
|
43
|
+
const base = { inject: undefined };
|
|
44
|
+
return new Proxy(base, {
|
|
45
|
+
get(target, key) {
|
|
46
|
+
if (key in target) return target[key];
|
|
47
|
+
if (typeof key === 'symbol') return undefined;
|
|
48
|
+
throw new Error(`cannot get property "${String(key)}" without inject`);
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
38
53
|
export async function verify({ dir, mount = true }) {
|
|
39
54
|
const failures = [];
|
|
40
55
|
const report = doctor(dir);
|
|
@@ -113,6 +128,7 @@ export async function verify({ dir, mount = true }) {
|
|
|
113
128
|
// 0.8.0 shipped a package whose root export was the CLI, installed cleanly,
|
|
114
129
|
// and mounted nothing.
|
|
115
130
|
const rootFile = rootEntry(report.manifest);
|
|
131
|
+
let hostFace = null;
|
|
116
132
|
if (rootFile !== null) {
|
|
117
133
|
const absolute = path.join(installed, rootFile);
|
|
118
134
|
if (fs.existsSync(absolute) === false) {
|
|
@@ -126,6 +142,7 @@ export async function verify({ dir, mount = true }) {
|
|
|
126
142
|
try {
|
|
127
143
|
const { pathToFileURL } = await import('node:url');
|
|
128
144
|
const loaded = await import(pathToFileURL(absolute).href);
|
|
145
|
+
hostFace = loaded;
|
|
129
146
|
if (typeof loaded.apply !== 'function') {
|
|
130
147
|
return {
|
|
131
148
|
ok: false,
|
|
@@ -144,6 +161,26 @@ export async function verify({ dir, mount = true }) {
|
|
|
144
161
|
}
|
|
145
162
|
}
|
|
146
163
|
|
|
164
|
+
// A host face that reads a service it never declared does not get undefined
|
|
165
|
+
// back: cordis throws "cannot get property ... without inject", the row fails,
|
|
166
|
+
// and the plugin manager shows one red dot. This cost a real user several
|
|
167
|
+
// restarts, so the activation itself is checked, not just the import.
|
|
168
|
+
if (hostFace !== null) {
|
|
169
|
+
try {
|
|
170
|
+
hostFace.apply(undeclaredContext());
|
|
171
|
+
} catch (error) {
|
|
172
|
+
return {
|
|
173
|
+
ok: false,
|
|
174
|
+
stage: 'install',
|
|
175
|
+
failures: [{
|
|
176
|
+
check: 'host apply',
|
|
177
|
+
message: `apply() throws when a service it never declared is read, which fails the row: ${error.message}`,
|
|
178
|
+
}],
|
|
179
|
+
files: packed.files,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
147
184
|
// `--no-mount` skips the DOM, not the install: the host half is the half
|
|
148
185
|
// that silently fails, and importing it needs nothing but Node.
|
|
149
186
|
if (mount === false) return { ok: true, stage: 'pack', failures: [], files: packed.files, size: packed.size };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-toolkit",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.7",
|
|
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": {
|