Files
BrowserSkill/packages/dsh-plugin-browserskill/tsdown.config.ts
T
BB-fat 6b96fcca32 feat(dsh-plugin-browserskill): add the web client half with a browser_screenshot toolview
Make the package dual-face: a dsh.client declaration (platform 'web')
plus an exports["./client"] bundle built in dsh's closure-factory shape
(window.__ModuleLoader__.load handoff, platform modules external,
everything else inlined, CSS Modules via lightningcss).

The client registers a keyed 'tool.call.toolview' view for
browser_screenshot that keeps the terminal block (command + output) and,
when the settled result carries an image block, resolves the durable
attachment through the client session's authorized readAttachment RPC
and renders it with the shared MessageImage thumbnail/lightbox atoms.
Path-only results render unchanged; every other browser_* tool keeps
the stock terminal card.

Tests: view-model derivation (running/ok/error, image vs path-only),
component rendering through @testing-library/react + happy-dom, and the
registration key. biome.json now excludes the package's lib/ build
output (same treatment as dist/).
2026-08-14 02:54:33 +00:00

125 lines
5.0 KiB
TypeScript

import { readFile } from "node:fs/promises";
import { basename, dirname, resolve as resolvePath } from "node:path";
import { transform } from "lightningcss";
import { defineConfig, type UserConfig } from "tsdown";
/**
* Two build faces of the dual-face package:
* - host: the Cordis tool plugin (lib/index.mjs, ESM, dsh packages external);
* - client: the Web UI bundle (lib/client.js) in dsh's closure-factory shape —
* the artifact registers itself through `window.__ModuleLoader__.load`, with
* platform modules resolved from the loader's frozen module table and every
* other dependency inlined. Mirrors deepseek-harness `packages/client/tsdown.client.ts`.
*/
/** Module specifiers the dsh web shell shares into its frozen module table. */
const CLIENT_EXTERNALS: readonly string[] = [
"react",
"react/jsx-runtime",
"react-dom",
"react-dom/client",
"@deepseek-ai/cordis",
"@deepseek-ai/dsh-client-ui-slots",
"@deepseek-ai/dsh-client-web-react",
"@deepseek-ai/dsh-client-ui-primitives",
"@deepseek-ai/dsh-client-ui-attachment",
"@deepseek-ai/dsh-client-schema-form",
"@deepseek-ai/dsh-client-runtime/client",
];
/** Wire/type layers with no shared runtime identity: safe to inline. */
const INLINE_SAFE = /^@deepseek-ai\/dsh-(host-apiproxy|session|llm|tools|brand|attachment)(\/|$)/;
const VENDORED_LIBRARY = /^@deepseek-ai\/(cosmokit|schemastery)(\/|$)/;
const CSS_VIRTUAL_PREFIX = "\0bsk-css:";
const CSS_VIRTUAL_SUFFIX = ".mjs";
const CLIENT_ID = "dsh-plugin-browserskill";
const client: UserConfig = {
name: `${CLIENT_ID}/client`,
entry: { client: "src/client/index.ts" },
outDir: "lib",
format: "cjs",
platform: "browser",
dts: false,
clean: false,
external: [...CLIENT_EXTERNALS],
// Anything not in the loader module table must inline; an unanswerable
// require() is a guaranteed runtime throw.
noExternal: (id: string) => (CLIENT_EXTERNALS.includes(id) ? undefined : true),
define: {
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV ?? "production"),
"import.meta.env.MODE": JSON.stringify(process.env.NODE_ENV ?? "production"),
"import.meta.env": JSON.stringify({ MODE: process.env.NODE_ENV ?? "production" }),
},
plugins: [
{
// Bundle purity gate: platform modules stay external, wire layers
// inline, every other @deepseek-ai value import is a build error.
name: "bsk-client-bundle-purity",
resolveId(source: string) {
if (!source.startsWith("@deepseek-ai/")) return null;
if (CLIENT_EXTERNALS.includes(source)) return null;
if (VENDORED_LIBRARY.test(source)) return null;
if (INLINE_SAFE.test(source)) return null;
throw new Error(
`client bundle purity: "${source}" is not a platform module or an inline-safe wire layer — ` +
"collaborate through cordis services (type-only imports are erased and never reach this gate)",
);
},
},
{
name: "bsk-css-modules-inline",
resolveId(source: string, importer: string | undefined) {
if (!source.endsWith(".module.css")) return null;
const abs = importer !== undefined ? resolvePath(dirname(importer), source) : source;
return CSS_VIRTUAL_PREFIX + abs + CSS_VIRTUAL_SUFFIX;
},
async load(virtualId: string) {
if (!virtualId.startsWith(CSS_VIRTUAL_PREFIX)) return null;
const fileId = virtualId.slice(CSS_VIRTUAL_PREFIX.length, -CSS_VIRTUAL_SUFFIX.length);
this.addWatchFile(fileId);
const source = await readFile(fileId);
const { code, exports: cssExports } = transform({
filename: fileId,
code: source,
cssModules: { pattern: "[hash]_[local]" },
minify: true,
});
const classMap: Record<string, string> = {};
for (const [local, exp] of Object.entries(cssExports ?? {})) classMap[local] = exp.name;
return [
`const css = ${JSON.stringify(code.toString())};`,
`const tagId = ${JSON.stringify(`${CLIENT_ID}/${basename(fileId)}`)};`,
"if (typeof document !== 'undefined' && document.querySelector('style[data-plugin-css=' + JSON.stringify(tagId) + ']') === null) {",
" const tag = document.createElement('style');",
` tag.dataset.plugin = ${JSON.stringify(CLIENT_ID)};`,
" tag.dataset.pluginCss = tagId;",
" tag.textContent = css;",
" document.head.appendChild(tag);",
"}",
`export default ${JSON.stringify(classMap)};`,
].join("\n");
},
},
],
outputOptions: {
entryFileNames: "client.js",
banner: `window.__ModuleLoader__.load({ id: ${JSON.stringify(CLIENT_ID)}, factory: (require) => {`,
footer: "return module.exports; } });",
intro: "var module = { exports: {} }; var exports = module.exports;",
},
};
export default defineConfig([
{
entry: ["src/index.ts"],
format: ["esm"],
dts: true,
outDir: "lib",
// dsh host packages are provided by the profile the plugin is installed into.
external: [/^@deepseek-ai\//],
},
client,
]);