bundesk
v0.4.2
Published
Fast Bun framework for building, running, integrating, and updating local web apps as desktop applications.
Maintainers
Readme
BunDesk
English · 中文
用 Bun 和系统浏览器,把本地 Web 应用变成启动快、构建快、容易调试的桌面应用。
BunDesk 是一个面向 Bun 的桌面应用框架,而不只是 EXE 打包脚本。名称由 Bun + Desktop 组成。框架统一处理 HTTP server、浏览器窗口、单实例、自动升级、Windows 文件关联与开始菜单集成,同时保留底层组合式 API。
为什么是 BunDesk
构建很快
BunDesk 的正式构建是一次 Bun.build({ compile }):TypeScript、server、浏览器资源和 Bun runtime 直接生成单文件可执行程序。它不需要编译 Rust/C++ 桌面壳,也不复制一套 Chromium,因此避免了 Tauri 原生依赖编译和 Electron renderer/runtime 打包中最重的步骤。
实际耗时仍取决于应用规模、插件和网络缓存;建议在具体项目中记录 CI 基线。BunDesk 的框架测试会真实构建并运行 Windows/Linux 可执行文件,而不是只测试配置对象。
交付物是单个 binary
BunDesk 将 Bun runtime、server 和由入口导入的前端资源编进同一个平台可执行文件;发布时只需复制这一个 binary。Electron 即使提供单文件安装器,安装后通常仍会展开为包含 Electron/Chromium、app.asar、DLL、locale 和 resources 的应用目录。BunDesk 不携带浏览器目录,应用数据则按运行时约定保存在当前用户数据目录,不与发布物混放。
实际项目基准会同时记录发布文件数、解包/安装后总大小和压缩下载大小,避免只比较安装器表面的单文件形式。
调试直接
开发时应用就是普通 Bun HTTP server 和普通网页:
- server 代码直接由 Bun 运行,可使用现有 TypeScript 调试方式;
- UI 使用浏览器自带 DevTools,不经过自定义调试桥;
--no-window可只启动 server,再用任意浏览器或 API 客户端调试;- 应用 routes、Bun 插件、Vite/Tailwind 和 Worker 构建逻辑都留在应用仓库中。
支持交叉构建
可以在 Linux CI 上生成 Windows x64/ARM64 单文件 EXE。BunDesk 下载与构建 Bun 同版本的 Windows runtime,先跨平台写入图标、版本资源和 manifest,再通过 executablePath 完成 Bun 编译。Windows 构建机不是必需条件。
当前产物是可直接分发的单文件 EXE。MSI、MSIX 或安装向导不是 0.1 版的产物格式。
不附带 Chromium
运行时优先使用系统已安装的 Microsoft Edge、Google Chrome 或 Chromium,并以 --app=<url> 打开独立应用窗口;未找到时使用隔离且可跟踪的 profile 启动 Firefox,最后才回退到系统 URL opener。每次选择或失败都会输出日志。收益是更小的发布物、更少的 renderer 更新负担和更短的打包链路。
与 Electron / Tauri 的定位
| | BunDesk | Electron | Tauri | | --- | --- | --- | --- | | 应用后端 | Bun | Node.js | Rust + 可选 sidecar | | Renderer | 系统 Edge/Chrome/Chromium | 随应用附带 Chromium | 系统 WebView | | 正式构建主链路 | Bun bundle + compile | JS bundle + Electron packaging | 前端构建 + Rust/native compile | | Linux 构建 Windows 单文件 EXE | 支持 | 依赖目标打包配置 | 通常需要额外交叉工具链 | | 调试 | Bun + 浏览器 DevTools | Electron DevTools | WebView DevTools + Rust 调试 | | 原生能力 | Bun/Node API + Windows 集成模块 | Electron API | Tauri 插件/Rust |
BunDesk 适合“本地 HTTP 服务 + Web UI”的工具型桌面应用。需要深度原生 UI、系统级沙箱或随应用固定 Chromium 版本时,应选择更匹配的方案。
核心功能
createDesktopApp(...)一体化托管 server、窗口和生命周期;openDesktopWindow(...)等组合式底层 API;- Edge/Chrome/Chromium
--app=<url>独立窗口(macOS 含 Brave),Termux 走 Android VIEW intent; - 带随机 256-bit token 的 loopback IPC 单实例;
- 次实例把
argv、cwd和 PID 转发给主实例回调; - 静态二进制 URL/ETag/SHA-256 和 GitHub Releases 两种升级 provider;
- 下载校验、原子替换、失败回滚、重启和旧版本清理;
- 系统通知(Windows WinRT toast,经 PowerShell 桥;Linux notify-send / macOS osascript / Termux termux-notification);
- 系统托盘(Windows 已实现:纯 bun:ffi 调 Win32;Linux 已实现:StatusNotifierItem over D-Bus 纯 JS 客户端;均无原生编译);
- 服务注册(headless
serve常驻):Windows HKCU Run key、Linux systemd user unit、macOS launchd LaunchAgent、Termux boot 脚本; - Windows 当前用户文件关联、默认打开方式和开始菜单快捷方式;
- Linux XDG 文件关联、desktop entry 和 mimeapps 注册(register/unregister/status);
- macOS
.app打包:Info.plist、UTI/文档类型、URL scheme、图标与 ad-hoc codesign; - Windows
detached/hidden/inherit三种控制台策略; - Linux 交叉构建 Windows x64、baseline x64 和 ARM64,以及 macOS x64/ARM64
.app; - 构建结果大小与 SHA-256 输出。
安装
bun add -d bundesk包名 bundesk 可直接用于 Bun:
import { createDesktopApp, defineConfig } from 'bundesk'要求 Bun 1.4.0 或更新版本。
运行时快速开始
应用 entrypoint:
import {
createDesktopApp,
githubReleaseProvider,
} from 'bundesk'
const app = createDesktopApp({
id: 'my-company.my-app',
version: '1.2.3',
server: {
hostname: '127.0.0.1',
port: 0,
routes: {
'/': new Response('<h1>My App</h1>', {
headers: { 'content-type': 'text/html; charset=utf-8' },
}),
'/api/health': Response.json({ ok: true }),
},
},
window: {
provider: 'chromium-app',
path: '/',
preferred: 'edge',
exitWithWindow: true,
},
singleInstance: {},
async onSecondInstance(event, context) {
console.log('Second launch:', event.argv, event.cwd)
await context.launchWindow()
},
updates: {
currentVersion: '1.2.3',
provider: githubReleaseProvider({
owner: 'OWNER',
repository: 'REPOSITORY',
assetName: {
'windows-x64': 'my-app.exe',
'linux-x64': 'my-app',
'darwin-arm64': 'My App.app.zip',
},
}),
checkOnStartup: false,
},
desktopIntegration: {
fileAssociations: [{
extension: '.demo',
progId: 'MyCompany.MyApp.Document',
description: 'My App Document',
}],
startMenuShortcut: {
name: 'My App',
description: 'Open My App',
},
},
})
await app.run()框架保留以下应用命令:
my-app 启动 server 和配置的具体窗口 provider
my-app --help 显示根据配置生成的帮助并退出
my-app --version 显示应用名称和版本并退出
my-app --provider webview2 显式固定一个具体 provider(禁用配置的 fallback)
my-app --no-window 不打开窗口
my-app <file> 启动或把文件参数转发给主实例
my-app serve --no-window 只启动 HTTP server
my-app register [--default] 注册当前用户文件关联和 launcher
my-app unregister 取消注册
my-app status 查看桌面集成状态
my-app install-service 注册为开机自启服务(headless serve)
my-app uninstall-service 移除服务注册
my-app service-status 查看服务状态
my-app install-pwa 打开安装 URL 并等待用户在浏览器中确认
my-app install-pwa --policy 通过 Edge/Chrome 企业策略强制安装
my-app remove-pwa-policy 仅移除本应用的强制安装策略条目
my-app upgrade [--force] 检查、安装升级并重启-h 等价于 --help,-V 等价于 --version。可用 cli.name、cli.description 和 cli.options 定制帮助中显示的名称、说明和应用专属选项;框架命令会自动列出。
register 只写 HKCU,不要求管理员权限。--default 写当前用户的扩展名默认 ProgID,但不会绕过 Windows 的 UserChoice 保护。
示例应用
example-app/ 是一个交互式 playground,在一个可运行应用里配置 BunDesk 的全部功能(不随 npm 包发布)。页面本身是 fullstack 路由,并链接了各功能的实时 JSON 端点:
- 全栈页面(HTML import——开发时热更新,编译产物 AOT),并提供 PWA manifest、service worker 和运行时生成的 PNG 图标
- 应用自行定义的窗口 provider 策略:Windows 为
webview2→ Chromium → Firefox,Linux 为webkitgtk→ Chromium → Firefox,macOS 为 Chromium → Firefox;同时支持--provider固定任意具体 provider - 组合式 API 提供的 provider matrix 和窗口句柄事实(
/api/providers) - PWA 安装:
install-pwa、install-pwa --policy、remove-pwa-policy和chromium-pwa窗口 - 单实例:次实例的
argv、cwd、PID 转发到主实例回调,并记录在页面中、重新打开窗口 - 自动升级:默认使用 GitHub Releases provider(开启
structuralUpdates),可通过BUNDESK_EXAMPLE_UPDATE_URL切换静态二进制 provider;页面提供检查按钮,CLI 提供upgrade命令 - 托盘(Windows + Linux)、通知(嵌入窗口 bridge 与 HTTP API 两条路径)、桌面集成、服务注册、粘性端口和解析出的运行环境
- 构建配置覆盖 Windows 元数据/图标/控制台模式、Linux,以及带文档类型、URL scheme 和生成
.icns图标的 macOS.app
cd example-app
bun run dev # 打开桌面窗口(dev 环境,HMR 生效)
bun run smoke # 无头服务/功能检查,不开窗口
bun run second-instance # dev 运行时转发 argv/cwd/PID 到主实例
bun run help # 查看生成的 CLI 帮助(含 PWA/更新命令)
bun run build # 构建当前平台的产物
bun run build:win # 强制 Windows 目标
bun run build:macos # 强制构建两个 macOS .app 目标package.json 还提供 serve、register、unregister、status、service:install、service:uninstall、service:status、pwa:install、pwa:policy、pwa:remove-policy 和 upgrade 快捷脚本。
Playground 环境变量:
BUNDESK_EXAMPLE_PORT— 默认端口(43123);传--port 0可体验粘性随机端口BUNDESK_EXAMPLE_VERSION— 覆盖内嵌应用版本(构建时默认使用框架package.json的版本)BUNDESK_EXAMPLE_DATA_DIR— 应用数据根目录(单实例、粘性端口、隔离的浏览器/PWA profile)BUNDESK_EXAMPLE_UPDATE_URL— 切换为静态二进制升级地址(可选BUNDESK_EXAMPLE_UPDATE_VERSION和BUNDESK_EXAMPLE_UPDATE_CHANGELOG_URL)BUNDESK_EXAMPLE_CONSOLE— 构建 Windows executable 时选择detached(默认)、hidden或inherit- Windows 交叉构建会把下载的 Bun runtime 缓存到
example-app/.cache/bundesk-runtime(可通过bundesk.config.ts的runtime字段调整)
CI(.github/workflows/ci.yml)在各平台原生 runner 上构建(Windows x64、Linux x64、macOS arm64 + x64),并对源码与编译产物分别做冒烟测试。
组合式 API
不使用一体化入口时,可以单独组合:
import {
acquireSingleInstance,
createUpdater,
findChromiumBrowser,
findFirefoxBrowser,
openDesktopWindow,
registerWindowsIntegration,
staticBinaryProvider,
} from 'bundesk'这些模块与 createDesktopApp 使用同一实现,不存在第二套行为。
运行环境(development / production)
框架解析应用环境并以 context.env('development' | 'production')暴露。该模式只填充默认值——任何显式配置的行为始终优先。
解析优先级(从高到低):
- 命令行:
my-app --mode=production(或--mode production) BUNDESK_ENV环境变量——框架专用覆盖项,应用如需保留NODE_ENV给自己用,可独立钉住它NODE_ENV环境变量(标准)- 默认:打包后的单二进制为 production,bun 宿主运行为 development
onReady: (context) => {
if (context.env === 'production') {
// 精简日志、关闭调试端点、……
}
}当前该模式驱动:
Bun.serve({ development })—— 默认context.env === 'development'(渲染错误页、上下文异常)。在server选项中显式设置development: false可无视模式钉死。
非 development/production 的值永远不会被框架消费:命令行 --mode=staging 仍是应用的参数,NODE_ENV=staging 也仍可被应用读取——框架只认这两个标准值。
粘性随机端口
动态端口默认具有粘性。当 server.port 未设置或设为 0 时,首次启动由操作系统随机选择空闲端口,并记录到 <appData>/server-port.json;后续启动优先复用该端口,使本地 URL 跨重启保持稳定。若端口已被占用,BunDesk 会自动回退到新的随机端口并更新记录。
显式的非零 server.port 或非零命令行 --port 始终优先,且不会读写粘性端口记录。设置 stickyPort: false 可保留随机分配但禁用复用;便携模式或测试也可覆盖记录目录:
server: {
port: 0,
stickyPort: { dataDirectory: './portable-data' }, // 默认 true
fetch: () => new Response('Hello'),
}Unix socket 模式
设置 server.unix 可在完全无 TCP listener 的情况下以无头模式运行。
server.port、server.hostname、粘性端口及命令行 --port/--host 均不适用。
窗口和 PWA provider 需要浏览器可加载的 HTTP origin,因此应不配置 window,
或显式设置 window: false:
const app = createDesktopApp({
id: 'my-company.headless-app',
server: {
unix: '/run/my-app.sock',
fetch: () => Response.json({ ok: true }),
},
window: false,
singleInstance: {},
})
const session = await app.start()
const response = await fetch('http://localhost/status', { unix: session.unix })context.unix 是真实端点;context.url 使用描述性的 http+unix: scheme,
刻意不可直接 fetch。启用 single-instance 后,二次启动会通过同一个 socket 的
POST /second-instance 转发,并保留 bearer-token 鉴权;BunDesk 不再另开
loopback TCP listener。
Bun 原生 routes(包括 HTML import bundle)可正常用于 unix listener。
Bun 1.4 也能正确路由 Unix socket 请求,即使设置 HTTP_PROXY 或
HTTPS_PROXY 导致 fetch(url, { unix }) 发出 absolute-form request target。
BunDesk 带鉴权的 single-instance IPC 会注册在同一个原生 routes table 中。
全栈页面(HTML imports)
Bun 的打包器可以直接从 HTML 文件提供完整前端管线:import 一个 .html 文件并作为路由传入——Bun 会自动打包其中所有 <script> 与 <link> 标签(TypeScript/TSX/JSX/CSS),把标记重写为哈希资源 URL 并提供。
import dashboard from './src/dashboard.html'
const app = createDesktopApp({
id: 'my-company.my-app',
server: {
port: 0,
routes: {
'/': dashboard,
'/api/data': () => Response.json({ ok: true }),
},
},
window: { provider: 'chromium-app' },
})无需自定义前端打包脚本——页面资源属于应用的构建图(bundesk 编译时以 AOT 方式产出同一批资源)。
Tailwind CSS v4
BunDesk 不内置或封装 Tailwind 编译器,而是使用 Bun 官方的静态路由插件,让同一份源 CSS 在开发和编译时都进入 Bun 的 HTML 管线。
在应用中安装依赖:
bun add -d tailwindcss bun-plugin-tailwind增加项目级 bunfig.toml,确保插件在 Bun.serve() 打包 HTML route 前加载:
[serve.static]
plugins = ["bun-plugin-tailwind"]HTML 直接引用源 CSS:
/* src/page/app.css */
@import "tailwindcss";
@source "../components/";<link rel="stylesheet" href="./app.css" />编译应用时,通过 defineConfig 已暴露的标准 Bun build 配置传入同一个插件:
import tailwind from 'bun-plugin-tailwind'
import { defineConfig } from 'bundesk'
export default defineConfig({
entrypoint: 'src/main.ts',
outfile: 'dist/my-app.exe',
plugins: [tailwind],
})开发脚本随后可直接运行应用(bun src/main.ts):Bun 会随 HTML route 重新生成 Tailwind CSS,并通过 HMR 更新已打开的页面,不再需要提交生成后的 CSS,也不需要单独运行 tailwindcss --watch。
静态插件必须通过 [serve.static] 加载;动态调用 Bun.plugin(tailwind) 并不等价,因为 runtime plugin builder 没有 Tailwind 插件需要的原生 onBeforeParse hook。删除已有 Tailwind CLI 管线前还应对比生成结果:插件发行版可能内嵌不同版本的 Tailwind 编译器。此前在 Bun 1.3.14 下实测,即使安装了 [email protected],[email protected] 仍生成带 Tailwind 4.1.14 banner 的 CSS;若必须与指定编译器版本完全一致,应继续使用 CLI watcher。
参见 Bun Tailwind 插件文档和 bun-plugin-tailwind。
dev 与 prod 行为
该管线由运行环境切换(见上节):框架的 development 默认值正是 Bun 全栈服务器使用的开关。
| 特性 | dev(bun server/main.ts) | prod(编译二进制) |
| --- | --- | --- |
| 资源打包 | 每次请求重新打包 | 缓存(dev 关闭时)/ AOT manifest(编译) |
| Source map | ✅ | ❌ |
| 压缩 | ❌ | ✅ |
| 热模块替换 | ✅(WebSocket 运行时织入客户端) | ❌ |
| 错误详情 | 详细 | 精简 |
已在 Bun 1.4.0 实测:dev 响应带 sourceMappingURL 与 HMR 客户端;编译单二进制输出压缩后的 chunk-<hash>.js/css。
开发循环
开发时桌面窗口(webview/webkit)加载 dev server,前端改动直接热更进已打开的窗口——无需重启应用:
bun server/main.ts # 窗口打开,HMR 生效
# 修改 src/dashboard.html 或其脚本 → 窗口就地更新在 server 选项中加 development: { console: true },可把页面 console.log 经 HMR 连接回显到终端。
服务端 bun --hot
使用 bun --hot 启动应用,可在不重启 Bun 进程的情况下软重载后端模块:
bun --hot src/main.tsapp.run() 会自动识别 hot 模式:启动完成后立即返回,避免入口模块求值永久 pending;下一轮模块求值开始时,BunDesk 会先停止旧应用 session(server、窗口、托盘和单实例锁),再在同一进程、同一显式或粘性端口上启动替代 session。
它与 Bun 的浏览器 HMR 互补:server.development 更新 HTML/TSX/CSS 客户端,bun --hot 重新求值后端代码和生命周期配置。若希望每次修改都获得完全隔离的进程重启,仍可使用 bun --watch。
入口继续写 await app.run() 即可。不要再套一层永不结束的 Promise 或 interval;入口模块必须完成求值,Bun 才能应用下一次软重载。hot 模式仅用于开发,编译二进制仍保持正常的阻塞生命周期。
平台集成
Linux:XDG 文件关联与 launcher
register / unregister / status 在 Linux 上写入 XDG 标准位置,全部为当前用户级:
- MIME 包:
~/.local/share/mime/packages/<appId>.xml(扩展名 →application/x-<progId>),随后尽力刷新update-mime-database; - desktop entry:
~/.local/share/applications/<appId>.desktop(Exec="<exe>" %F、MimeType=); - 默认关联:
~/.config/mimeapps.list的[Added Associations](--default时写入[Default Applications])。
my-app register [--default]
my-app unregister
my-app status没有 update-mime-database 时注册仍然成功,只是 MIME 缓存不刷新。
macOS:构建期 .app 打包
macOS 没有运行期注册:文件关联、URL scheme 和图标在构建时写进 bundle 的 Info.plist,register/status 命令返回明确的 unsupported 说明。
export default defineConfig({
entrypoint: 'server/main.ts',
outfile: 'dist/My App.app',
target: 'bun-darwin-arm64',
macos: {
bundleIdentifier: 'com.mycompany.myapp',
displayName: 'My App',
version: '1.2.3',
icon: 'src/app/AppIcon.icns',
documentTypes: [{ extension: '.demo', name: 'My App Document' }],
urlTypes: [{ scheme: 'myapp' }],
background: false,
codesign: false, // 默认在 macOS 主机上做 ad-hoc 签名;false 跳过
},
})outfile以.app结尾时生成 bundle:Contents/MacOS/<name>为可执行文件,Contents/Info.plist含CFBundleDocumentTypes、UTExportedTypeDeclarations(自动导出 UTI)和CFBundleURLTypes。- 非
.app的 darwinoutfile保持单文件 Mach-O 形态。 - 在 macOS 主机上默认执行
codesign --force --deep -s -(ad-hoc);交叉构建产物不会签名,分发前必须在 Mac 上签名并公证(codesign+notarytool)。 - Linux CI 同样可以交叉构建 macOS x64/ARM64
.app(Bun 下载同版本 darwin runtime)。
Termux(Android)
BunDesk 检测到 Termux 环境($PREFIX 指向 com.termux 数据目录)时:
- 窗口不再是 Chromium
--app,而是 Android VIEW intent(am start或termux-open-url),由系统浏览器打开 URL; - 应用生命周期、单实例、HTTP server、自动升级与普通平台一致;
exitWithWindow在 Termux 下不生效(intent 立即返回)。
注意:Bun 运行时需能在 Termux 中执行(glibc proot 环境,如 proot-distro 内的 Debian/Ubuntu),浏览器侧无额外要求。
注册为服务
因为 app 自带 API 层并能 serve,它可以作为常驻 headless 服务注册:开机/登录自动启动、不弹窗口、API 一直在线。GUI 交互通过单实例 IPC 转发到服务进程,由 onSecondInstance 决定 launchWindow() 连回同一 server。
my-app install-service # 注册并立即启动
my-app service-status # 查看注册与运行状态
my-app uninstall-service # 停止并移除| 平台 | 机制 | 说明 |
| --- | --- | --- |
| Windows | HKCU Run key | 登录自启,无需管理员;真正的 SCM 服务需要原生 StartServiceCtrlDispatcher,Bun 无法提供 |
| Linux | systemd user unit | ~/.config/systemd/user/<appId>.service,systemctl --user enable --now;无需 root |
| macOS | launchd LaunchAgent | ~/Library/LaunchAgents/<appId>.plist,launchctl bootstrap gui/<uid>;日志写入应用数据目录 |
| Termux | termux-boot 脚本 | ~/.termux/boot/<appId>.sh,由 Termux:Boot 在开机时执行 |
约定:
- 服务以
"<exe>" serve --no-window运行,注册时固化可执行文件路径;框架的原子自升级在同一路径替换文件,服务无需重新注册; service-status的active字段通过单实例记录(instance.json+ PID 存活)判断,跨平台一致;install-service/uninstall-service支持--dry-run预览;- 服务使用
WorkingDirectory/RunAtLoad/Restart=on-failure/KeepAlive保证崩溃拉起,应用内的相对路径应基于process.execPath解析而非 cwd。
系统托盘
const app = createDesktopApp({
id: 'my-company.my-app',
server: { port: 0, routes: { '/': new Response('Hello') } },
tray: {
icon: 'src/app/tray.ico', // Windows:.ico 或可执行文件路径;默认系统图标
tooltip: 'My App',
menu: [
{ label: '打开主窗口', onClick: (context) => context.launchWindow() },
{ separator: true },
{ label: '退出', onClick: (context) => context.stop() },
],
onActivate: (context) => context.launchWindow(), // 左键点击
},
})- 配置托盘后,关闭窗口默认不退出(
exitWithWindow默认为 false),应用驻留托盘;托盘菜单里调用context.stop()退出; - 交互回调(
onActivate、菜单onClick)与 action 一样拿到完整context; - 托盘图标可运行期更新:
context.tray?.update({ tooltip: '...', icon: '...' }),context.tray?.destroy()移除。
平台现状:
| 平台 | 状态 | 机制 |
| --- | --- | --- |
| Windows | 已实现 | 纯 bun:ffi 调 user32/shell32:Shell_NotifyIconW + 隐藏窗口 + 50ms 消息泵,无原生工具链 |
| macOS | 未实现 | AppKit NSStatusItem 经 objc_msgSend FFI(需 NSApplication/run-loop 配合,可行但脆弱) |
| Linux | 已实现 | StatusNotifierItem over D-Bus:纯 JS D-Bus 客户端(EXTERNAL 认证、wire 编解码)+ com.canonical.dbusmenu;需 session bus 与支持 SNI 的宿主(KDE/XFCE/GNOME + AppIndicator);不支持的 daemon 优雅降级为无托盘 |
| Termux | 不支持 | Android 无托盘概念 |
Windows 上新注册的图标可能先出现在溢出区(Windows 默认行为),用户拖到主托盘即可;iconPresent() 探测对溢出区隐藏图标按文档返回 false。
系统通知
const app = createDesktopApp({
id: 'my-company.my-app',
server: { port: 0, routes: { '/': new Response('Hello') } },
notifications: { aumid: 'MyCompany.MyApp' }, // 可选:toast 归属的 AppUserModelID
})
// 应用内任意位置
await context.notify({
title: '构建完成',
body: 'release 产物已生成',
})平台机制(context.notify 返回是否投递成功):
| 平台 | 机制 | 点击回调 |
| --- | --- | --- |
| Windows | WinRT toast,经 PowerShell 桥(Windows.UI.Notifications) | 未实现(需 AUMID 注册 + activation 处理) |
| Linux | notify-send(libnotify,icon 走 -i) | 无 |
| macOS | osascript display notification | 无 |
| Termux | termux-notification(termux-api) | 无 |
已知取舍:
- 经典
Shell_NotifyIcon气泡在 Windows 10/11 已被抑制(实测NIM_MODIFY返回成功但屏幕无任何显示,WinForms 对照同样不显示),所以 Windows 走 toast; - 默认 toast 以 "Windows PowerShell" 为来源名;配置
{ aumid }并以该 AUMID 创建开始菜单快捷方式后,toast 以你的应用名义出现; - 点击回调需要 toast activation(启动参数 + 前台激活),列入 roadmap。
PWA 安装与窗口
具体 chromium-pwa provider 通过 Chromium app id 启动
Edge/Chrome/Brave/Chromium Web App;BunDesk 也可以辅助完成首次安装:
const app = createDesktopApp({
id: 'my-company.my-app',
// PWA 必须使用稳定 origin,不能在这里使用动态端口。
server: { port: 43123, routes: { '/': page } },
window: {
provider: 'chromium-pwa',
exitWithWindow: false,
preferred: 'edge',
pwa: {
appId: 'abcdefghijklmnopabcdefghijklmnop',
profileDirectory: 'Default',
// 默认使用正在运行的应用 URL;外部托管的 PWA 可显式指定。
// installUrl: 'https://app.example.com/',
installTimeoutMs: 300_000,
policy: {
createDesktopShortcut: true,
customName: 'My App',
},
// 标准浏览器可省略;这是浏览器 user-data 根目录,不是单个 profile 目录。
// userDataDir: 'C:/Users/me/AppData/Local/Microsoft/Edge/User Data',
},
},
})交互式安装
my-app install-pwaBunDesk 启动配置的 server,在指定浏览器 profile 中打开安装 URL,等待用户接受浏览器 原生安装提示。安装完成通过 profile 文件系统事件检测,不使用固定 sleep 轮询;PWA 已经安装时立即返回。若 server 已由另一个应用实例持有,该命令会转发给主实例并返回 主实例的执行结果。
企业策略安装
my-app install-pwa --policy
my-app install-pwa --policy --dry-run
my-app remove-pwa-policyWindows 上,策略模式把该 URL 合并到当前用户 Microsoft Edge 或 Google Chrome 的
强制 WebAppInstallForceList 注册表策略中。它保留无关条目,要求浏览器刷新指定
profile,并等待配置的 appId 出现。该策略是 mandatory policy;条目生效期间,受
影响的用户不能自行卸载这个 PWA。
若机器级 WebAppInstallForceList 已包含该 URL,BunDesk 直接使用它,不再添加用户
策略;若机器策略包含另一份列表,BunDesk 拒绝用用户策略遮蔽它,并要求交由管理员部署。
remove-pwa-policy 只移除 URL 匹配的当前用户条目并保留其他条目;机器级强制条目
不会被移除。移除策略要求不会卸载已经存在的 PWA。自动修改策略目前刻意只支持
Windows。Chrome 在 Linux 和
macOS、Edge 在 macOS 也支持同一策略,但这些系统要求管理员部署 policy 文件或配置
描述文件;BunDesk 不尝试提权。
官方格式参见
Microsoft Edge WebAppInstallForceList 策略
和 Google Chrome 策略列表。
启动行为与约束
- BunDesk 用
--app-id、--user-data-dir和--profile-directory启动已安装应用, 不会回退成 URL App Mode。 - 安装状态通过
Web Applications/Manifest Resources/<appId>确认;配置的appId不匹配时会超时并明确报错。 - Web App Manifest、Service Worker、图标、scope 和
start_url仍由应用负责。 - 必须使用固定 server origin。已安装 PWA 从 manifest 的
start_url启动;window.path或运行时重新选择的动态端口无法重定向已安装应用。 - Windows、Linux、macOS 上的标准 Edge、Chrome、Brave、Chromium 可自动推断
userDataDir;便携版或非标准浏览器应显式配置。 - 共用 browser profile 时,请求可能交给已经运行的浏览器;此时返回的 subprocess
只跟踪启动器而非真实 PWA 窗口。应设置
exitWithWindow: false让后端独立存活。 - 交互安装和启动支持 Edge、Chrome、Brave、Chromium;自动策略安装只支持 Windows 上的 Edge 和 Chrome。Firefox 与 Termux 不支持。
CLI --provider chromium-pwa 固定已安装 PWA 窗口 provider;它不会执行安装命令,也不会选择其他 provider。
具体窗口 provider 与显式 fallback
BunDesk 不会把平台映射成 provider,不会追加隐式 fallback,也没有默认 provider。
省略 window 就不会打开窗口。应用直接指定一个具体实现:
const app = createDesktopApp({
id: 'my-company.my-app',
server: {
port: 0,
routes: {
'/': new Response('<h1>Hello</h1>', {
headers: { 'content-type': 'text/html; charset=utf-8' },
}),
},
},
window: {
provider: 'webview2',
fallback: [
{ provider: 'chromium-app', on: ['unsupported', 'unavailable'] },
{ provider: 'firefox-window', on: ['unsupported', 'unavailable'] },
],
path: '/',
width: 900,
height: 640,
title: 'My App',
onMessage: (message) => console.log('page says:', message),
},
})primary provider、fallback provider、顺序及允许 fallback 的失败类别都由应用决定。
未配置 fallback 时,provider 失败即为最终结果。--provider <id> 显式固定
一个 provider 并禁用配置的 fallback;--no-window 不打开窗口。
可用的具体 ID:
| Provider | 机制 | 能否观察真实窗口关闭 |
| --- | --- | --- |
| webview2 | Windows 进程内 WebView2 | 能 |
| webkitgtk | Linux 进程内 WebKitGTK | 能 |
| chromium-app | 隔离 Chromium --app=<url> 进程 | 能,针对受管进程 |
| chromium-pwa | 已安装 Chromium app id | 不能;进程可能只是 launcher |
| firefox-window | 隔离 Firefox profile/window 进程 | 能,针对受管进程 |
| system-browser | 系统 URL opener | 不能 |
| android-view-intent | Termux Android VIEW intent | 不能;进程只是 dispatcher |
webview2 和 webkitgtk 提供 navigate、executeScript、postMessage、
导航就绪和 page-to-host 消息。页面必须返回真实
content-type: text/html。webview2 使用系统 WebView2 Runtime,由 Bun 内嵌
TinyCC 编译无头文件 C shim;它刻意绕过 WebView2Loader.dll,发现 Edge 统一
runtime 后直调未文档化的 CreateWebViewEnvironmentWithOptionsInternal。
webkitgtk 需要 WebKit2GTK 4.1 栈和桌面 display。
返回的 DesktopWindowHandle 由 provider 和 kind 判别,提供 ready、
closed、lifecycle、capabilities、close() 和完整 attempts 轨迹。
嵌入式 handle 额外提供脚本、消息和导航方法。ready 记录真实证据:
navigation-completed、process-started 或 launch-dispatched。
只读取事实而不触发框架选择:
const report = await inspectWindowProvider('webview2')
const releaseEvidence = getWindowProviderMatrix()report 分开报告 target 兼容性、当前机器可用性、release 验证状态、能力和结构化
诊断。release 矩阵只是证据;两个 API 都不会选择或替换 provider。
当前嵌入式 provider 证据:
| Provider | Target | 实现 | 验证 |
| --- | --- | --- | --- |
| webview2 | Windows x64 | 已实现 | 实验性;原生创建/导航/脚本/消息/关闭 smoke 已通过 |
| webview2 | Windows arm64 | 未实现 | 当前实现要求 runtime TinyCC;测试的 compiled Bun runtime 不提供 |
| webkitgtk | Linux x64 | 已实现 | WSLg 原生创建/导航/脚本/消息/关闭 smoke 已通过 |
| webkitgtk | Linux arm64 | 已实现 | 未验证 |
| wkwebview | macOS | 未实现 | 不暴露 provider ID |
选择的 provider 无法观察真实窗口关闭时,exitWithWindow: true 会被拒绝。
chromium-pwa、system-browser 和 android-view-intent 应设为 false。
自动升级
静态发布地址
适合对象存储、CDN 或普通 HTTP server:
import { staticBinaryProvider } from 'bundesk'
const provider = staticBinaryProvider({
binaryUrl: 'https://downloads.example/my-app.exe',
changelogUrl: 'https://downloads.example/CHANGELOG.txt',
version: '1.2.4',
structuralUpdates: true,
})provider 使用 HEAD 的 ETag 检查当前文件;支持 SHA-256 ETag、普通 MD5 和兼容 16 MiB 分片的对象存储 ETag。下载阶段还会校验 Content-Length、X-Checksum-SHA256 / Digest、可选 descriptor SHA-256,以及 Windows EXE 的 MZ 文件头。
GitHub Releases
import { githubReleaseProvider } from 'bundesk'
const provider = githubReleaseProvider({
owner: 'OWNER',
repository: 'REPOSITORY',
structuralUpdates: true,
assetName: 'my-app.exe',
})provider 比较当前版本与 release tag,选择指定 asset,并使用 GitHub asset digest(存在时)校验下载。
无索引 section 级 Range 更新
在更新 provider 设置 structuralUpdates: true。发布 executable 不做任何
修改:BunDesk 不生成内嵌索引、JSON sidecar 或针对旧版本的 delta。正常
构建并签名的同一个 executable 同时用于直接下载和增量更新。
更新时,BunDesk 通过 HTTP Range 解析远程 PE/ELF/Mach-O header 和 section
table。目标 runtime section 的 container identity、section name/index 和
size 与当前 executable 一致时,客户端乐观地从本地复制。Header、signature、
平台资源、空隙和完整 .bun section 始终下载。BunDesk 不保存 module
contents hash;任意内嵌 JS、CSS、HTML 或 shim 变化都会下载完整 .bun
section。大型可变资源应使用应用层更新,不应依赖 Bun compiled graph。
Layout 相同只作为复用提示,不作为可信证明。可信 release metadata 必须
提供最终 artifact SHA-256。BunDesk 重建临时 executable,只有完整 size
和 SHA-256 都匹配才安装。若 Bun runtime 已变化、但某个 section 恰好仍
同名同 size,乐观重建会在摘要校验时失败,默认 fallbackToFull 随后下载
完整 artifact。不支持精确 206 Partial Content、稳定 Content-Range、
Accept-Encoding: identity 或不可变 ETag 的 server 也会进入完整回退。
这种设计让普通 app-only release 在零发布 metadata、零 executable 修改
的前提下复用体积很大的 Bun runtime。runtime 变化的 release 可能先发生
一次局部尝试,再完整下载;只有明确不接受这一权衡时才设置
fallbackToFull: false。
macOS 下该模式描述 Mach-O executable。完整 .app 更新仍需处理
Info.plist、resources 和 bundle signature;不能把 executable updater
直接指向 .app 的 ZIP 归档。
单实例安全模型
BunDesk 不把实例转发接口暴露在应用 routes 中。框架单独启动只绑定 127.0.0.1 的 IPC HTTP server,并为每次主实例生成 256-bit 随机 token。token 只写入当前用户应用数据目录的权限受限文件;次实例必须携带 Bearer token 才能转发参数。崩溃留下的 lock/record 会在确认原 PID 已退出后清理。
构建配置
在项目根目录创建 bundesk.config.ts:
import { defineConfig } from 'bundesk'
export default defineConfig({
entrypoint: 'server/main.ts',
outfile: 'dist/my-app.exe',
target: 'bun-windows-x64',
minify: true,
define: {
__APP_VERSION__: JSON.stringify('1.2.3'),
},
windows: {
console: 'detached',
icon: 'src/app/icon.ico',
title: 'My App',
publisher: 'My Company',
version: '1.2.3',
description: 'My local desktop web app',
copyright: 'Copyright (C) 2026 My Company',
},
})构建:
bunx bundesk
bunx bundesk --config build/bundesk.config.ts
bunx bundesk --target bun-windows-x64-baseline配置文件可以导出数组,一次生成多个平台产物。应用自己的 Tailwind/Vite/Worker 插件直接通过标准 plugins 传入,BunDesk 不复制应用构建逻辑。
Windows 控制台模式
| windows.console | 行为 | 场景 |
| --- | --- | --- |
| detached(默认) | 双击不分配控制台;终端启动时继承现有终端 | 同时提供 GUI 和 CLI |
| hidden | 使用 Bun hideConsole,按 GUI 程序运行 | 纯 GUI |
| inherit | 保留 Bun 默认控制台行为 | CLI 优先 |
detached 通过 Windows consoleAllocationPolicy manifest 实现。BunDesk 先修改干净的 bun.exe 再编译,避免在 Bun payload 已追加后重写 PE 文件。
交叉构建 runtime
Windows 本机且架构一致时,默认复用当前 bun.exe。Linux 交叉构建或 baseline/ARM64 构建会下载:
https://github.com/oven-sh/bun/releases/download/bun-v<Bun.version>/<target>.zip可通过 runtime.downloadUrl 使用自定义镜像,通过 runtime.sha256 固定解压后 bun.exe 的校验值。
平台范围
| 功能 | Windows | Linux | macOS | Termux (Android) |
| --- | --- | --- | --- | --- |
| HTTP server / 生命周期 | 支持 | 支持 | 支持 | 支持 |
| 浏览器 / 进程内 WebView 窗口 | 支持 / 支持 | 支持 / 支持(WebKitGTK) | 支持 / 不支持 | VIEW intent / 不支持 |
| 已安装 Chromium PWA | 支持 | 支持 | 支持 | 不支持 |
| 安全单实例与参数转发 | 支持 | 支持 | 支持 | 支持 |
| 单文件构建 / .app bundle | 单文件 EXE | 单文件 | .app bundle | n/a |
| 交叉构建 | 任意平台 → EXE | 任意平台 → 单文件 | Linux/macOS → .app | n/a |
| 自动替换当前可执行文件 | 支持 | 底层 API 可用,0.1 不作桌面发布承诺 | 底层 API 可用,0.1 不作桌面发布承诺 | 底层 API 可用 |
| 文件关联 / launcher | 支持(HKCU) | 支持(XDG) | 构建期 Info.plist | 不支持 |
| 服务注册(headless serve) | HKCU Run key | systemd user | launchd agent | termux-boot |
| 系统托盘 | 支持(Win32 FFI) | 支持(SNI D-Bus) | 计划(AppKit FFI) | 不支持 |
| 系统通知 | WinRT toast(PowerShell 桥) | notify-send | osascript | termux-notification |
Windows 控制台模式(detached/hidden/inherit)仅 Windows 有效;windows/runtime 构建选项要求 bun-windows-* 目标,macos 选项要求 bun-darwin-* 目标且 outfile 以 .app 结尾。
Roadmap
完整方案见 应用迁移与性能基准计划。当前只完成选型和实验设计,尚未开始迁移或采集性能数据。
已完成(本轮):
- macOS 运行时支持(浏览器候选、数据目录、darwin 升级 asset)与
.appbundle 构建(Info.plist、UTI/文档类型、URL scheme、图标、ad-hoc codesign); - Linux XDG 文件关联、desktop entry、mimeapps 注册(
register/unregister/status); - Termux(Android)检测与 VIEW intent 窗口;
- 服务注册(Windows Run key / systemd / launchd / termux-boot)、Windows 系统托盘(纯 Win32 FFI)与系统通知(WinRT toast 桥、notify-send、osascript、termux-notification)。
待评估:
- 第一轮:draw.io Desktop(Electron)、NextChat(Tauri)、NeoHtop(Tauri)、LLMPET(Electron),覆盖 static-heavy、web-first、native-backend 和 small-but-real-backend(状态机 + 计量 + 权限)四类应用;
- 第二轮:MarkText(Electron)、Yaak(Tauri),扩大文件系统、编辑器、数据库、网络、插件和 secret/keychain 的兼容性边界;
- macOS 签名/公证流水线在真实 Mac CI 上的落地;
- Hermes Agent + Poly:评估以 Poly 在同一进程中承载 Bun 与 RustPython,将 Hermes Agent 的 Python agent/runtime 与 BunDesk 桌面壳整合;
- Oh My Pi + Poly:评估将 Oh My Pi 的 Bun/TypeScript 主体直接接入 BunDesk,并通过 Poly 承载 Python 工具内核。
开发与验证
bun install
bun run typecheck
bun test
bun run pack:check测试覆盖:真实 Windows/Linux 单文件构建与执行、macOS .app 交叉构建结构(Mach-O、Info.plist)、Windows PE metadata/manifest、真实 Chromium App Mode 进程、安全单实例转发、Linux XDG 注册往返、静态升级安装、GitHub release provider,以及 Windows 注册表 dry-run。
License
MIT
