npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

bundesk

v0.4.2

Published

Fast Bun framework for building, running, integrating, and updating local web apps as desktop applications.

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 单实例;
  • 次实例把 argvcwd 和 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.namecli.descriptioncli.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-pwainstall-pwa --policyremove-pwa-policychromium-pwa 窗口
  • 单实例:次实例的 argvcwd、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 还提供 serveregisterunregisterstatusservice:installservice:uninstallservice:statuspwa:installpwa:policypwa:remove-policyupgrade 快捷脚本。

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_VERSIONBUNDESK_EXAMPLE_UPDATE_CHANGELOG_URL
  • BUNDESK_EXAMPLE_CONSOLE — 构建 Windows executable 时选择 detached(默认)、hiddeninherit
  • Windows 交叉构建会把下载的 Bun runtime 缓存到 example-app/.cache/bundesk-runtime(可通过 bundesk.config.tsruntime 字段调整)

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')暴露。该模式只填充默认值——任何显式配置的行为始终优先。

解析优先级(从高到低):

  1. 命令行:my-app --mode=production(或 --mode production
  2. BUNDESK_ENV 环境变量——框架专用覆盖项,应用如需保留 NODE_ENV 给自己用,可独立钉住它
  3. NODE_ENV 环境变量(标准)
  4. 默认:打包后的单二进制为 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.portserver.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_PROXYHTTPS_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.ts

app.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>.desktopExec="<exe>" %FMimeType=);
  • 默认关联:~/.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.plistregister/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.plistCFBundleDocumentTypesUTExportedTypeDeclarations(自动导出 UTI)和 CFBundleURLTypes
  • .app 的 darwin outfile 保持单文件 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 starttermux-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>.servicesystemctl --user enable --now;无需 root | | macOS | launchd LaunchAgent | ~/Library/LaunchAgents/<appId>.plistlaunchctl bootstrap gui/<uid>;日志写入应用数据目录 | | Termux | termux-boot 脚本 | ~/.termux/boot/<appId>.sh,由 Termux:Boot 在开机时执行 |

约定:

  • 服务以 "<exe>" serve --no-window 运行,注册时固化可执行文件路径;框架的原子自升级在同一路径替换文件,服务无需重新注册;
  • service-statusactive 字段通过单实例记录(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 NSStatusItemobjc_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-pwa

BunDesk 启动配置的 server,在指定浏览器 profile 中打开安装 URL,等待用户接受浏览器 原生安装提示。安装完成通过 profile 文件系统事件检测,不使用固定 sleep 轮询;PWA 已经安装时立即返回。若 server 已由另一个应用实例持有,该命令会转发给主实例并返回 主实例的执行结果。

企业策略安装

my-app install-pwa --policy
my-app install-pwa --policy --dry-run
my-app remove-pwa-policy

Windows 上,策略模式把该 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 |

webview2webkitgtk 提供 navigateexecuteScriptpostMessage、 导航就绪和 page-to-host 消息。页面必须返回真实 content-type: text/htmlwebview2 使用系统 WebView2 Runtime,由 Bun 内嵌 TinyCC 编译无头文件 C shim;它刻意绕过 WebView2Loader.dll,发现 Edge 统一 runtime 后直调未文档化的 CreateWebViewEnvironmentWithOptionsInternalwebkitgtk 需要 WebKit2GTK 4.1 栈和桌面 display。

返回的 DesktopWindowHandleproviderkind 判别,提供 readyclosedlifecyclecapabilitiesclose() 和完整 attempts 轨迹。 嵌入式 handle 额外提供脚本、消息和导航方法。ready 记录真实证据: navigation-completedprocess-startedlaunch-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-pwasystem-browserandroid-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-RangeAccept-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)与 .app bundle 构建(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