feat: ship prebuilt plugin bundle for remote dsh install

Remote github installs only get packages/dsh-plugin; without bundle the
host tried pnpm build and hung. Pack web+server into bundle/ and start
from it so other machines can run after dsh plugin add.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
oliver 2026-09-12 10:45:08 +08:00
parent d95f033d1f
commit b23959400d
13 changed files with 28325 additions and 41 deletions

View file

@ -1,15 +1,30 @@
# dsh-mind-map 本地安装到 DSH web profile(开发用)
# dsh-mind-map 安装
```powershell
$plugin = "d:\project\chatgpt\mind-map\packages\dsh-plugin"
# 用 dsh plugin add 链到本地包,或在 profile 里 link
dsh plugin --profile web add -w $plugin
## 远程安装(推荐)
仓库需包含预构建 `packages/dsh-plugin/bundle`(维护者执行 `pnpm pack:plugin` 后推送)。
```bash
dsh plugin --profile web add -w "github:hansjone/mind-map#path:packages/dsh-plugin"
```
装好后:
装好后重启 `dsh web`,硬刷新浏览器。依赖:`dsh-better-sidebar`。
1. 重启 `dsh web`,浏览器硬刷新(Ctrl+Shift+R)
2. 右侧栏「+」选 **思维导图**,或 设置 → Mind Map → **打开思维导图 Tab**
3. 拓扑占满右侧内容区;对话继续用中间 DSH 输入框
启动日志应出现:
依赖:已安装 `dsh-better-sidebar`(提供 `ctx.betterSidebar.registerTab`)。
```text
[dsh-mind-map] starting packaged bundle/server.mjs
```
若仍出现 `building web UI…`,说明装到的版本没有 `bundle/`,请更新插件或改用本地完整仓库。
## 本地开发(link 整个 monorepo)
```powershell
cd d:\project\chatgpt\mind-map
pnpm install
pnpm pack:plugin
dsh plugin --profile web add -w "d:\project\chatgpt\mind-map\packages\dsh-plugin"
```
开发改 UI 时可继续用 monorepo 源码启动(无 bundle 时会自动 `pnpm --filter @mind-map/web build`)。

View file

@ -0,0 +1 @@
Prebuilt assets for remote dsh plugin install. Regenerated by: node packages/dsh-plugin/scripts/pack-bundle.mjs

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Mind Map</title>
<script type="module" crossorigin src="/assets/index-iTIYRavW.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BK6FV7kG.css">
</head>
<body>
<div id="root"></div>
</body>
</html>

View file

@ -2,6 +2,10 @@
* Start / stop the mind-map Hono server as a child process.
* Lifetime is tied to the DSH host: when the plugin disposes / process exits,
* the canvas server is killed (including process tree on Windows).
*
* Two layouts:
* - Packaged (remote `dsh plugin add`): packages/dsh-plugin/bundle/{web,server.mjs}
* - Monorepo (local link): ../../.. mind-map root with apps/web + apps/server
*/
import { spawn, execSync } from 'node:child_process'
@ -11,7 +15,26 @@ import { fileURLToPath } from 'node:url'
import { createRequire } from 'node:module'
const __dirname = dirname(fileURLToPath(import.meta.url))
export const mindMapRoot = join(__dirname, '../../..')
/** dsh-mind-map package root (…/dsh-mind-map or …/packages/dsh-plugin) */
export const pluginRoot = join(__dirname, '..')
/** Monorepo root when linked from source; otherwise same as pluginRoot when packaged. */
export const mindMapRoot = (() => {
const mono = join(__dirname, '../../..')
if (existsSync(join(mono, 'apps', 'web', 'package.json'))) return mono
return pluginRoot
})()
function bundledServer() {
return join(pluginRoot, 'bundle', 'server.mjs')
}
function bundledWebIndex() {
return join(pluginRoot, 'bundle', 'web', 'index.html')
}
function isPackagedInstall() {
return existsSync(bundledServer()) && existsSync(bundledWebIndex())
}
function resolveTsxCliRel() {
const candidates = [
@ -33,6 +56,9 @@ function resolveTsxCliRel() {
}
function ensureWebDist() {
// Remote/plugin install ships prebuilt assets — never run pnpm here.
if (isPackagedInstall()) return
const indexHtml = join(mindMapRoot, 'apps', 'web', 'dist', 'index.html')
const watchSrc = [
join(mindMapRoot, 'apps', 'web', 'src', 'TopologyCanvas.tsx'),
@ -55,6 +81,16 @@ function ensureWebDist() {
}
}
if (!needsBuild) return
if (!existsSync(join(mindMapRoot, 'pnpm-workspace.yaml'))) {
throw new Error(
'mind-map web UI is missing and this is not a monorepo checkout. ' +
'Reinstall a release that includes packages/dsh-plugin/bundle ' +
'(github:hansjone/mind-map#path:packages/dsh-plugin after maintainer runs pnpm pack:plugin), ' +
'or clone the full repo and link locally.',
)
}
console.log('[dsh-mind-map] building web UI (apps/web/dist missing or stale)…')
try {
execSync('pnpm --filter @mind-map/web build', {
@ -187,14 +223,7 @@ export function createRuntime() {
await new Promise((r) => setTimeout(r, 400))
}
const entryAbs = join(mindMapRoot, 'apps/server/src/index.ts')
const distAbs = join(mindMapRoot, 'apps/server/dist/index.js')
const useTs = existsSync(entryAbs)
const scriptAbs = useTs ? entryAbs : distAbs
if (!existsSync(scriptAbs)) {
throw new Error(`mind-map server entry not found under ${mindMapRoot}`)
}
const packaged = isPackagedInstall()
const env = {
...process.env,
PORT: String(port),
@ -205,18 +234,35 @@ export function createRuntime() {
}
if (dataDir) env.DATA_DIR = dataDir
const scriptRel = useTs ? 'apps/server/src/index.ts' : 'apps/server/dist/index.js'
let cwd
let args
if (useTs) {
const tsxCli = resolveTsxCliRel()
if (!tsxCli) throw new Error('tsx not found; run pnpm install in mind-map')
args = [tsxCli, scriptRel]
if (packaged) {
cwd = pluginRoot
env.MINDMAP_ROOT = pluginRoot
env.MINDMAP_WEB_DIST = join(pluginRoot, 'bundle', 'web')
args = [bundledServer()]
console.log('[dsh-mind-map] starting packaged bundle/server.mjs')
} else {
args = [scriptRel]
cwd = mindMapRoot
const entryAbs = join(mindMapRoot, 'apps/server/src/index.ts')
const distAbs = join(mindMapRoot, 'apps/server/dist/index.js')
const useTs = existsSync(entryAbs)
const scriptAbs = useTs ? entryAbs : distAbs
if (!existsSync(scriptAbs)) {
throw new Error(`mind-map server entry not found under ${mindMapRoot}`)
}
const scriptRel = useTs ? 'apps/server/src/index.ts' : 'apps/server/dist/index.js'
if (useTs) {
const tsxCli = resolveTsxCliRel()
if (!tsxCli) throw new Error('tsx not found; run pnpm install in mind-map')
args = [tsxCli, scriptRel]
} else {
args = [scriptRel]
}
}
child = spawn(process.execPath, args, {
cwd: mindMapRoot,
cwd,
env,
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
@ -277,8 +323,9 @@ export function createRuntime() {
...last,
running: Boolean(child && !child.killed),
pid: child?.pid || null,
packaged: isPackagedInstall(),
}
}
return { start, stop, status, mindMapRoot }
return { start, stop, status, mindMapRoot, pluginRoot }
}

View file

@ -11,11 +11,14 @@
},
"files": [
"lib",
"bundle",
"cordis.patch.yml",
"README.md"
"README.md",
"INSTALL.md"
],
"scripts": {
"test": "node --test test/*.test.js"
"test": "node --test test/*.test.js",
"pack": "node scripts/pack-bundle.mjs"
},
"dsh": {
"bundle": {

View file

@ -0,0 +1,71 @@
/**
* Pack prebuilt web UI + server into packages/dsh-plugin/bundle
* so `dsh plugin add github:…#path:packages/dsh-plugin` works without monorepo.
*/
import { execSync } from "node:child_process";
import {
cpSync,
existsSync,
mkdirSync,
rmSync,
writeFileSync,
} from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import * as esbuild from "esbuild";
const pluginRoot = dirname(dirname(fileURLToPath(import.meta.url)));
const repoRoot = join(pluginRoot, "../..");
const bundleRoot = join(pluginRoot, "bundle");
const webOut = join(bundleRoot, "web");
const serverOut = join(bundleRoot, "server.mjs");
function run(cmd, cwd = repoRoot) {
console.log(`[pack] ${cmd}`);
execSync(cmd, { cwd, stdio: "inherit", env: process.env, windowsHide: true });
}
// 1) Build workspace libs + web
run("pnpm --filter @mind-map/shared build");
run("pnpm --filter @mind-map/graph-core build");
run("pnpm --filter @mind-map/graph-store build");
run("pnpm --filter @mind-map/layout-engine build");
// dream-skin ships TS+JSON; esbuild resolves it without a separate tsc emit
run("pnpm --filter @mind-map/web build");
const webDist = join(repoRoot, "apps/web/dist");
if (!existsSync(join(webDist, "index.html"))) {
throw new Error("apps/web/dist/index.html missing after build");
}
// 2) Fresh bundle dir
rmSync(bundleRoot, { recursive: true, force: true });
mkdirSync(webOut, { recursive: true });
cpSync(webDist, webOut, { recursive: true });
// 3) Bundle server (workspace + npm deps inlined; node builtins stay external)
console.log("[pack] esbuild apps/server → bundle/server.mjs");
await esbuild.build({
entryPoints: [join(repoRoot, "apps/server/src/index.ts")],
bundle: true,
platform: "node",
format: "esm",
outfile: serverOut,
packages: "bundle",
banner: {
js: "import { createRequire as __mmCr } from 'node:module'; const require = __mmCr(import.meta.url);",
},
logLevel: "info",
});
if (!existsSync(serverOut)) {
throw new Error("bundle/server.mjs was not produced");
}
writeFileSync(
join(bundleRoot, "README.txt"),
"Prebuilt assets for remote dsh plugin install. Regenerated by: node packages/dsh-plugin/scripts/pack-bundle.mjs\n",
"utf8",
);
console.log("[pack] OK →", bundleRoot);