dsh-plugin-toolkit 0.8.1 → 0.8.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/CHANGELOG.md +22 -0
- package/lib/cli.mjs +2 -1
- package/lib/doctor.mjs +35 -0
- package/lib/verify.mjs +41 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
## 0.8.3
|
|
2
|
+
|
|
3
|
+
`verify` now imports the host half it just installed.
|
|
4
|
+
|
|
5
|
+
- After installing the tarball, `verify` imports the module the loader will
|
|
6
|
+
import (`exports["."]`, else `main`) and requires an `apply` function. A root
|
|
7
|
+
module that throws on import — which `doctor`'s static read cannot see — is
|
|
8
|
+
now a failure: `the installed index.js does not import: boom`.
|
|
9
|
+
- `--no-mount` skips the DOM, not the install, so the host half is checked in
|
|
10
|
+
both modes. The success line says which half ran.
|
|
11
|
+
|
|
12
|
+
## 0.8.2
|
|
13
|
+
|
|
14
|
+
`doctor` now refuses the failure that shipped in 0.8.0.
|
|
15
|
+
|
|
16
|
+
- New check `root export`: when a package declares `dsh.bundle`, its root module
|
|
17
|
+
has to be the plugin. A root that is also a `bin` target — a CLI wearing a
|
|
18
|
+
plugin's clothes — is reported, and so is a root module with no `apply`.
|
|
19
|
+
pnpm reports success either way; the row just mounts nothing.
|
|
20
|
+
- `test/doctor.test.mjs` covers both shapes, and a bundle with no root module
|
|
21
|
+
(a subpath plugin) stays legal.
|
|
22
|
+
|
|
1
23
|
## 0.8.1
|
|
2
24
|
|
|
3
25
|
`0.8.0` installed and then did nothing: the loader starts a plugin by importing
|
package/lib/cli.mjs
CHANGED
|
@@ -108,7 +108,8 @@ export async function run(argv, io = console) {
|
|
|
108
108
|
if (command === 'verify') {
|
|
109
109
|
const result = await verify({ dir, mount: options.noMount !== true });
|
|
110
110
|
if (result.ok === true) {
|
|
111
|
-
|
|
111
|
+
const what = result.stage === 'mount' ? 'installed the tarball, mounted the panel' : 'installed the tarball and imported its host half';
|
|
112
|
+
io.log(`ok packed ${result.files.length} file(s), ${what}`);
|
|
112
113
|
if (typeof result.text === 'string') io.log(` ${result.text}`);
|
|
113
114
|
return 0;
|
|
114
115
|
}
|
package/lib/doctor.mjs
CHANGED
|
@@ -138,6 +138,40 @@ export function checkFilesList(manifest) {
|
|
|
138
138
|
return null;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
/**
|
|
142
|
+
* The loader starts a plugin by importing the bare package name. A package that
|
|
143
|
+
* ships both a CLI and a panel is one `exports` edit away from having its root
|
|
144
|
+
* point at the CLI — the row then mounts nothing, and pnpm reports success.
|
|
145
|
+
*/
|
|
146
|
+
/** The module the loader imports when it starts the bare package name. */
|
|
147
|
+
export function rootEntry(manifest) {
|
|
148
|
+
const root = typeof manifest.exports === 'object' && manifest.exports !== null ? manifest.exports['.'] : undefined;
|
|
149
|
+
const entry = typeof root === 'string' ? root : manifest.main;
|
|
150
|
+
if (typeof entry !== 'string') return null;
|
|
151
|
+
return entry.replace(/^\.\//, '');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function checkRootExport(dir, manifest) {
|
|
155
|
+
if (manifest.dsh === undefined || manifest.dsh === null) return null;
|
|
156
|
+
const target = rootEntry(manifest);
|
|
157
|
+
// A bundle that inserts a subpath plugin needs no root module at all.
|
|
158
|
+
if (target === null) return null;
|
|
159
|
+
const entry = target;
|
|
160
|
+
const bins = Object.values(manifest.bin ?? {}).filter((value) => typeof value === 'string');
|
|
161
|
+
if (bins.some((bin) => bin.replace(/^\.\//, '') === target) === true) {
|
|
162
|
+
return `exports["."] points at the CLI (${entry}), so the loader's row has no plugin to apply; point it at the module that exports apply`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const file = path.resolve(dir, target);
|
|
166
|
+
if (fs.existsSync(file) === false) return null;
|
|
167
|
+
const source = fs.readFileSync(file, 'utf8');
|
|
168
|
+
const exported = /export\s+(?:async\s+)?function\s+apply\b|export\s+(?:const|let|var)\s+apply\b|export\s*\{[^}]*\bapply\b|\bapply\s*[:=]\s*(?:async\s*)?(?:function|\()/.test(source);
|
|
169
|
+
if (exported === false) {
|
|
170
|
+
return `the root export ${entry} does not export apply, so the loader imports the package and mounts nothing`;
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
141
175
|
/** Runs every check and returns `{ failures, notes }`. */
|
|
142
176
|
export function doctor(dir) {
|
|
143
177
|
const { manifest } = readManifest(dir);
|
|
@@ -149,6 +183,7 @@ export function doctor(dir) {
|
|
|
149
183
|
['patch file', checkPatchFile(dir, manifest)],
|
|
150
184
|
['client entry', checkClientEntry(dir, manifest)],
|
|
151
185
|
['files list', checkFilesList(manifest)],
|
|
186
|
+
['root export', checkRootExport(dir, manifest)],
|
|
152
187
|
];
|
|
153
188
|
const failures = checks.filter(([, result]) => result !== null && typeof result === 'string');
|
|
154
189
|
return {
|
package/lib/verify.mjs
CHANGED
|
@@ -12,6 +12,7 @@ import path from 'node:path';
|
|
|
12
12
|
|
|
13
13
|
import { doctor, declaredPaths } from './doctor.mjs';
|
|
14
14
|
import { resolveEntry } from './dev.mjs';
|
|
15
|
+
import { rootEntry } from './doctor.mjs';
|
|
15
16
|
import { run } from './exec.mjs';
|
|
16
17
|
|
|
17
18
|
function packJson(dir) {
|
|
@@ -78,7 +79,6 @@ export async function verify({ dir, mount = true }) {
|
|
|
78
79
|
|
|
79
80
|
if (failures.length > 0) return { ok: false, stage: 'pack', failures, files: packed.files };
|
|
80
81
|
|
|
81
|
-
if (mount === false) return { ok: true, stage: 'pack', failures: [], files: packed.files, size: packed.size };
|
|
82
82
|
|
|
83
83
|
const scratch = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-plugin-verify-'));
|
|
84
84
|
let installed;
|
|
@@ -108,6 +108,46 @@ export async function verify({ dir, mount = true }) {
|
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
// The loader starts the host half by importing the bare package name. Doing
|
|
112
|
+
// the same import here is the only way to know the row will have an apply —
|
|
113
|
+
// 0.8.0 shipped a package whose root export was the CLI, installed cleanly,
|
|
114
|
+
// and mounted nothing.
|
|
115
|
+
const rootFile = rootEntry(report.manifest);
|
|
116
|
+
if (rootFile !== null) {
|
|
117
|
+
const absolute = path.join(installed, rootFile);
|
|
118
|
+
if (fs.existsSync(absolute) === false) {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
stage: 'install',
|
|
122
|
+
failures: [{ check: 'root export', message: `the tarball does not ship ${rootFile}, which package.json points the loader at` }],
|
|
123
|
+
files: packed.files,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const { pathToFileURL } = await import('node:url');
|
|
128
|
+
const loaded = await import(pathToFileURL(absolute).href);
|
|
129
|
+
if (typeof loaded.apply !== 'function') {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
stage: 'install',
|
|
133
|
+
failures: [{ check: 'root export', message: `the installed ${rootFile} exports no apply, so the loader's row mounts nothing` }],
|
|
134
|
+
files: packed.files,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
return {
|
|
139
|
+
ok: false,
|
|
140
|
+
stage: 'install',
|
|
141
|
+
failures: [{ check: 'root export', message: `the installed ${rootFile} does not import: ${error.message}` }],
|
|
142
|
+
files: packed.files,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// `--no-mount` skips the DOM, not the install: the host half is the half
|
|
148
|
+
// that silently fails, and importing it needs nothing but Node.
|
|
149
|
+
if (mount === false) return { ok: true, stage: 'pack', failures: [], files: packed.files, size: packed.size };
|
|
150
|
+
|
|
111
151
|
// Same convention the host uses: dsh.client.entry, then client.js.
|
|
112
152
|
const entryFile = resolveEntry(installed, report.manifest);
|
|
113
153
|
if (entryFile !== null) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-toolkit",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
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": {
|