bun-app-window 0.4.1 → 0.5.0
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 +69 -1
- package/README.md +21 -2
- package/README.zh-CN.md +25 -2
- package/README.zh-TW.md +24 -2
- package/dist/libshell.dll +0 -0
- package/package.json +1 -1
- package/src/shell/app.ts +18 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,73 @@ All notable changes to this project are documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.5.0] - 2026-10-21
|
|
8
|
+
|
|
9
|
+
**Upgrading is enough for the first two items — no code changes on your side.** The third changes how you build a single-file executable, and it fixes something that never worked.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Keyboard shortcuts work again.** `F11`, `F12`, `F5`, `Ctrl+R`, `Ctrl+0` and `Ctrl±` were all dead, while `Ctrl+wheel` zoom kept working. The cause was message routing, not the handling logic: `WM_KEYDOWN` is delivered to the **focused** window, and focus sits on WebView2's render child window (`Chrome_RenderWidgetHostHWND`), which does not forward unhandled keys to its parent. Every `handle_input_message` call site on the two window procedures was unreachable by real keystrokes. `Ctrl+wheel` survived only because `WM_MOUSEWHEEL` *is* forwarded to the parent by the focused window's `DefWindowProc`.
|
|
14
|
+
|
|
15
|
+
The bug is easy to misdiagnose, because `PostMessage(hwnd, WM_KEYDOWN, VK_F11, 0)` toggles fullscreen correctly — that experiment simply bypasses what broke.
|
|
16
|
+
|
|
17
|
+
The shell now subscribes to `ICoreWebView2Controller::add_AcceleratorKeyPressed`, which fires before the key reaches the page and does not depend on the focus chain. Key semantics live in one place (`handle_accelerator_key`), shared by the callback and the surviving `WM_KEYDOWN` path; only `KEY_DOWN` / `SYSTEM_KEY_DOWN` are handled (handling `KEY_UP` would toggle twice and look like "nothing installed"), and handled keys call `put_Handled(TRUE)`.
|
|
18
|
+
|
|
19
|
+
- **`FLAG_ACCEPT_DROP` now delivers full paths.** With the flag on, pages received a DOM `drop` carrying only `name` and `size`; the promised `shell-msg { t: "drop", paths: [...] }` never arrived. `DragAcceptFiles` only sets `WS_EX_ACCEPTFILES` on the **top-level** window, but OLE hit-testing walks to the deepest window under the cursor (`webview_widget` → `Chrome_WidgetWin_1` → `Chrome_RenderWidgetHostHWND` → `Intermediate D3D Window`, all measured `ACCEPTFILES=False`). WebView2 registers its own `IDropTarget` on the render child, takes the drop, and turns it into a DOM event — the top-level default drop target never sees it. The three `ChangeWindowMessageFilterEx` calls only allow messages *in*; they do not decide where a message *goes*.
|
|
20
|
+
|
|
21
|
+
The fix is upstream: `ICoreWebView2Controller4::put_AllowExternalDrop(FALSE)` steps aside from that internal target, so the OLE chain continues to the parent and `WM_DROPFILES` reaches `handle_drop`. No hand-written `IDropTarget` — that would fight WebView2's internals, and the layering can change between releases (which is exactly how this was hit). On an older runtime the `QueryInterface` fails and the shell keeps WebView2's default behaviour rather than blocking startup.
|
|
22
|
+
|
|
23
|
+
- **Closing the window no longer keeps the process alive for five more seconds.** `App.stop()` used `Promise.race([closed.promise, Bun.sleep(CLOSE_TIMEOUT_MS)])`. A race takes the first settled promise and **does not cancel the loser**, so on the normal path the 5-second `Bun.sleep` became an un-awaited, un-unref'd timer that held Bun's event loop open. Measured: `race returned @ 9 ms`, process exit `@ 5010 ms`; a real window close took 5043 ms. Now a cancellable `setTimeout` with `finally { clearTimeout(timer) }`, and no `AbortSignal` on `Bun.sleep` (unsupported).
|
|
24
|
+
|
|
25
|
+
One measurement note worth keeping: the criterion has to be **when the process exits**, not how long `stop()` takes. With `clearTimeout` removed, `stop()` still returned in 42 ms while the process took 5.9 s to exit.
|
|
26
|
+
|
|
27
|
+
- **The single-file executable actually embeds the native library.** `libInfo.source === "embedded"` was the documented acceptance criterion for shape B, and it had never been achievable: `src/host.ts` did not import `bun-app-window/embedded`, so the bundler recorded no `type: "file"` dependency, `resolveNativeLib` could not find embedded bytes, and the only surviving fallback was "put `libshell.dll` next to the exe" — which reports `source: "package"`, not `embedded`. Builds succeeded and the window came up, so nobody looked further.
|
|
28
|
+
|
|
29
|
+
Adding the side-effect import fixes it. Measured in an empty directory with only the `.exe`, after deleting `%LOCALAPPDATA%\BunAppWindow\runtime\`:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
ExitCode 0
|
|
33
|
+
[lib] source=embedded path=...\BunAppWindow\runtime\libshell-15ef6452e3fcc26f.dll
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
A second run reports `source=embedded` again and leaves the extracted file's mtime untouched (`reused === true`). The bundle grew from 96,158,208 to 97,165,824 bytes — the difference is exactly `dist/libshell.dll`. This is also the first time `ADR 0003`'s self-managed extraction had been exercised by a real executable.
|
|
37
|
+
|
|
38
|
+
### Changed
|
|
39
|
+
|
|
40
|
+
- **WebView2 C/C++ SDK upgraded from `1.0.1150.38` to `1.0.4258.31`.** `AllowExternalDrop` needs SDK `1.0.1185.39` or newer and did not exist in the previous vendored copy, which is why the drag-and-drop fix above was impossible before. `AdditionalObjects` and `ICoreWebView2File` arrived with it; they are not used, because they are reachable only through `postMessage`, and this shell keeps page communication on WebSocket. The old nupkg is removed and the new one is committed, so offline rebuilds behave exactly as before.
|
|
41
|
+
|
|
42
|
+
`CORE_WEBVIEW_TARGET_PRODUCT_VERSION` moves from `99.0.1150.38` to `154.0.4258.31`. This project already requires Bun ≥ 1.4.3, TypeScript ≥ 7.0.2, Windows x64 and a working WebView2 Runtime, and **does not aim for compatibility with older runtimes** — that is a decision, not an oversight.
|
|
43
|
+
|
|
44
|
+
- **`timeoutMs: 0` is now documented and guarded.** `pickFolder()` blocks the host thread for however long a person browses, so `connect({ timeoutMs: 0 })` is required when exposing it over IPC — otherwise the client rejects after 30 seconds while the host is still working, and a missing `.catch` makes that entirely silent. Both halves are now in the docs, and two assertions keep the contract: one behavioural (`timeoutMs: 0` still pending after 50 ms) and one static (no `||` fallback, which would turn `0` back into `30000`).
|
|
45
|
+
|
|
46
|
+
Worth knowing beyond the timeout: `pickFolder()` blocks the **Bun main thread**, so timers in that process do not fire and other IPC calls queue up while the dialog is open. Lowering the timeout surfaces the symptom earlier; it does not fix the blocking. Real concurrency means running it in a Worker.
|
|
47
|
+
|
|
48
|
+
### Documentation
|
|
49
|
+
|
|
50
|
+
- `docs/pitfalls.md` gains 2.13 (shortcuts: `WM_KEYDOWN` goes to the focused window) and 2.14 (drag and drop: the deepest window wins the OLE hit test), plus 4.9 for the `embedded` criterion that nothing satisfied. Each records the double-sided verification, including the probe mistakes made along the way — a probe that called `SetFocus` on the top-level window measured the old code path and reported success with the fix removed.
|
|
51
|
+
- `AGENTS.md` records both routing mechanisms as implementation rules and replaces the open question about minimum runtime with the compatibility decision above.
|
|
52
|
+
- Three READMEs gain the `timeoutMs` section and cross-reference it from the `pickFolder` row.
|
|
53
|
+
|
|
54
|
+
### Verification
|
|
55
|
+
|
|
56
|
+
| Command | Result |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| `bun check` | 68 files |
|
|
59
|
+
| `bun test` | 556 pass / 0 fail |
|
|
60
|
+
| `bun run scripts/gate.ts` | 4 stages, PASSED |
|
|
61
|
+
| `bun run scripts/gate.ts --clean` | PASSED (clean) |
|
|
62
|
+
| `bun run scripts/verify-dll.ts` | PASSED, `dist/libshell.dll` matches `native/shell.cc` |
|
|
63
|
+
| `scripts/probe-window-events.ts` | 21 checks |
|
|
64
|
+
| `scripts/probe-accelerator-keys.ts` | 6 checks |
|
|
65
|
+
| `scripts/probe-drop.ts` | 6 checks |
|
|
66
|
+
| shape B in an empty directory | exit 0, `source=embedded`, `reused=true` |
|
|
67
|
+
|
|
68
|
+
Each of the two new probes was verified in both directions: removing `install_accelerator_handler()` makes a real `F11` do nothing (1826 px → 1826 px), and removing `handle_drop(...)` leaves the `HDROP` handle alive. With the fix in place both pass.
|
|
69
|
+
|
|
70
|
+
### Still manual
|
|
71
|
+
|
|
72
|
+
Dragging a real file into a window to confirm the page sees the **full path**. A synthetic `HDROP` proves the message reaches `handle_drop`; it cannot prove that OLE hit-testing now resolves to the parent window, because that requires an actual cross-process drag.
|
|
73
|
+
|
|
7
74
|
## [0.4.1] - 2026-10-21
|
|
8
75
|
|
|
9
76
|
### Fixed
|
|
@@ -327,7 +394,8 @@ First public release.
|
|
|
327
394
|
- Not yet verified in managed environments that enforce AppLocker, WDAC, or third-party EDR behaviour blocking.
|
|
328
395
|
- No commercially trusted code signing: only a self-signed certificate has been exercised end to end.
|
|
329
396
|
|
|
330
|
-
[Unreleased]: https://github.com/TiancongLx/bun-app-window/compare/v0.
|
|
397
|
+
[Unreleased]: https://github.com/TiancongLx/bun-app-window/compare/v0.5.0...HEAD
|
|
398
|
+
[0.5.0]: https://github.com/TiancongLx/bun-app-window/compare/v0.4.1...v0.5.0
|
|
331
399
|
[0.4.1]: https://github.com/TiancongLx/bun-app-window/compare/v0.4.0...v0.4.1
|
|
332
400
|
[0.4.0]: https://github.com/TiancongLx/bun-app-window/compare/v0.3.0...v0.4.0
|
|
333
401
|
[0.3.0]: https://github.com/TiancongLx/bun-app-window/compare/v0.2.2...v0.3.0
|
package/README.md
CHANGED
|
@@ -97,7 +97,7 @@ Note: `createShell(opts)` only resolves and loads the native DLL without opening
|
|
|
97
97
|
| `reload()` / `openDevTools()` / `toggleFullscreen()` | Equivalent to `Ctrl+R`, `F12`, and `F11` respectively |
|
|
98
98
|
| `setBrowserUi(mask)` | Disables specific default browser behaviors using a bitwise OR of `BROWSER_UI_*` flags |
|
|
99
99
|
| `showContextMenu(x, y, items)` | Displays a native Win32 context menu at the specified screen coordinates (see below) |
|
|
100
|
-
| `pickFolder(title?)` | Opens a native folder selection dialog (synchronously blocks the calling thread) |
|
|
100
|
+
| `pickFolder(title?)` | Opens a native folder selection dialog (synchronously blocks the calling thread; **when exposed over IPC, pair it with `timeoutMs: 0`**, see below) |
|
|
101
101
|
| `libInfo` | Metadata of the resolved native library (file path, resolution source, SHA-256 hash) |
|
|
102
102
|
| `symbols` | Escape hatch providing direct access to the raw `dlopen` FFI symbols |
|
|
103
103
|
|
|
@@ -263,6 +263,25 @@ api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
|
263
263
|
|
|
264
264
|
By default, `connect()` extracts the auth token from the page URL's `?t=` query parameter and connects to `/ws`. Pass a custom `url` or `channel` option to override this behavior.
|
|
265
265
|
|
|
266
|
+
#### `timeoutMs` — long-running human interaction must opt out
|
|
267
|
+
|
|
268
|
+
Calls time out after **30 seconds** by default. That is right for ordinary methods and wrong for anything a human sits in front of:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
// Host: pickFolder wraps SHBrowseForFolder — synchronous, and however long the user takes
|
|
272
|
+
const service = { pickFolder: () => shell.pickFolder() };
|
|
273
|
+
|
|
274
|
+
// Page: you must disable the timeout, or the client rejects while the host is still working
|
|
275
|
+
const api = await connect<Api, Events>({ timeoutMs: 0 });
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
`0` means **no timeout** (not "a zero-millisecond timeout"). Two ways this bites, neither caught by any gate:
|
|
279
|
+
|
|
280
|
+
- The page forgets `.catch` → the rejection is completely silent; the UI just does nothing after the user picks.
|
|
281
|
+
- Cancelling returns quickly, so only a **slow** pick reproduces it — easy to write off as flaky.
|
|
282
|
+
|
|
283
|
+
One more layer worth knowing: `shell.pickFolder()` **blocks the Bun main thread**. While the dialog is open, timers in that process do not fire and other IPC calls queue up (they are not rejected — they simply get no answer). The client's 30-second timer lives in the browser renderer process and is unaffected, so lowering the timeout does not fix the blocking, only surfaces it sooner. Real concurrency means running `pickFolder` in a Worker.
|
|
284
|
+
|
|
266
285
|
#### Reserved client names
|
|
267
286
|
|
|
268
287
|
The client is a `Proxy`: a handful of names resolve to the client itself and never reach a remote
|
|
@@ -614,7 +633,7 @@ For window enumeration (`EnumWindows`) or automated screenshot scripts, filter b
|
|
|
614
633
|
## Acknowledgments
|
|
615
634
|
|
|
616
635
|
- Platform windowing layer based on [webview/webview](https://github.com/webview/webview) `0.12.0` (MIT License, Serge Zaitsev & Steffen André Langnes). `vendor/webview/` contains an unmodified upstream copy.
|
|
617
|
-
- WebView2 SDK provided by [Microsoft.Web.WebView2](https://www.nuget.org/packages/Microsoft.Web.WebView2) `1.0.
|
|
636
|
+
- WebView2 SDK provided by [Microsoft.Web.WebView2](https://www.nuget.org/packages/Microsoft.Web.WebView2) `1.0.4258.31` (NuGet package vendored for offline builds). The WebView2 Runtime is provided by the OS.
|
|
618
637
|
- Co-authored-by DeepSeek
|
|
619
638
|
|
|
620
639
|
## License
|
package/README.zh-CN.md
CHANGED
|
@@ -97,7 +97,7 @@ shell.setTitle("Doing work…");
|
|
|
97
97
|
| `reload()` / `openDevTools()` / `toggleFullscreen()` | 页面重载、打开开发者工具、切换全屏(分别对应 `Ctrl+R` / `F12` / `F11`) |
|
|
98
98
|
| `setBrowserUi(mask)` | 禁用指定的浏览器默认行为,传入 `BROWSER_UI_*` 标志位的按位或组合 |
|
|
99
99
|
| `showContextMenu(x, y, items)` | 在指定屏幕坐标弹出 Win32 原生上下文菜单(详见下文用法) |
|
|
100
|
-
| `pickFolder(title?)` |
|
|
100
|
+
| `pickFolder(title?)` | 调用原生文件夹选择对话框(同步阻塞当前调用线程;**经 IPC 暴露时务必配 `timeoutMs: 0`**,见下文) |
|
|
101
101
|
| `libInfo` | 原生动态库解析元数据(包含实际路径、加载来源、SHA-256 校验值) |
|
|
102
102
|
| `symbols` | 底层直通接口:直接访问通过 `dlopen` 导出的底层 C 符号 |
|
|
103
103
|
|
|
@@ -164,6 +164,7 @@ const app = await createApp({
|
|
|
164
164
|
browserUi: BROWSER_UI_ALL,
|
|
165
165
|
service: {
|
|
166
166
|
greet: (name: string) => `hi ${name}`,
|
|
167
|
+
// 人机交互的调用耗时不可控:页面侧必须 connect({ timeoutMs: 0 }),见 ipc 一节
|
|
167
168
|
pickFolder: () => app.shell.pickFolder(),
|
|
168
169
|
},
|
|
169
170
|
});
|
|
@@ -252,6 +253,28 @@ api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
|
252
253
|
|
|
253
254
|
默认情况下,`connect` 会从当前页面 URL 的查询参数 `?t=` 中读取鉴权 Token 并连接至 `/ws`。若需自定义连接方式,可显式传入 `url` 或自定义 `channel` 实例。
|
|
254
255
|
|
|
256
|
+
#### `timeoutMs` —— 长时人机交互必须显式关掉
|
|
257
|
+
|
|
258
|
+
单次调用默认 **30 秒**超时。这对普通方法合理,对**人机交互**是致命的:
|
|
259
|
+
|
|
260
|
+
```ts
|
|
261
|
+
// 服务端:pickFolder 内部是 SHBrowseForFolder,同步阻塞、耗时完全由用户决定
|
|
262
|
+
const service = { pickFolder: () => shell.pickFolder() };
|
|
263
|
+
|
|
264
|
+
// 页面端:必须关掉超时,否则用户挑久一点,客户端先 reject,而服务端还在正常阻塞
|
|
265
|
+
const api = await connect<Api, Events>({ timeoutMs: 0 });
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
`0` 表示**不超时**(不是「0 毫秒超时」)。踩坑的形态有两种,都不会让门禁变红:
|
|
269
|
+
|
|
270
|
+
- 页面没接 `.catch` → 拒绝完全静默,表现为「选完了界面毫无反应」。
|
|
271
|
+
- 用户点了「取消」很快返回,所以只有**挑久了**才复现,容易被判成偶发。
|
|
272
|
+
|
|
273
|
+
还有一层要知道:`shell.pickFolder()` **阻塞的是 Bun 主线程**。对话框打开期间,
|
|
274
|
+
同一进程里的定时器不会触发,其他 IPC 调用也会排队(不是 reject,是没有响应)。
|
|
275
|
+
客户端那 30 秒定时器跑在浏览器渲染进程里,不受影响——所以调短超时并不能解决阻塞,
|
|
276
|
+
只能让症状更早暴露。需要并发就得把 `pickFolder` 放进 Worker 里调。
|
|
277
|
+
|
|
255
278
|
#### 客户端保留名
|
|
256
279
|
|
|
257
280
|
客户端是一个 `Proxy`:有几个名字会解析到客户端自己身上,永远到不了远端方法。服务表里若有
|
|
@@ -591,7 +614,7 @@ bun run demo # 启动交互式自检示例程序(需在桌面会话下
|
|
|
591
614
|
## 致谢
|
|
592
615
|
|
|
593
616
|
- 底层跨平台窗口抽象源自 [webview/webview](https://github.com/webview/webview) `0.12.0`(MIT 许可,作者 Serge Zaitsev · Steffen André Langnes)。仓库内 `vendor/webview/` 目录为其源码原样拷贝,未作任何侵入式修改。
|
|
594
|
-
- WebView2 C/C++ SDK 源自 [Microsoft.Web.WebView2](https://www.nuget.org/packages/Microsoft.Web.WebView2) `1.0.
|
|
617
|
+
- WebView2 C/C++ SDK 源自 [Microsoft.Web.WebView2](https://www.nuget.org/packages/Microsoft.Web.WebView2) `1.0.4258.31`(NuGet 包随仓库分发以支持离线构建),运行时由操作系统提供。
|
|
595
618
|
- Co-authored-by DeepSeek
|
|
596
619
|
|
|
597
620
|
## 开源许可
|
package/README.zh-TW.md
CHANGED
|
@@ -97,7 +97,7 @@ shell.setTitle("Doing work…");
|
|
|
97
97
|
| `reload()` / `openDevTools()` / `toggleFullscreen()` | 頁面重新載入、開啟開發人員工具、切換全螢幕(分別對應 `Ctrl+R` / `F12` / `F11`) |
|
|
98
98
|
| `setBrowserUi(mask)` | 停用指定的瀏覽器預設行為,傳入 `BROWSER_UI_*` 旗標的按位元 OR 組合 |
|
|
99
99
|
| `showContextMenu(x, y, items)` | 於指定螢幕座標彈出 Win32 原生右鍵快顯功能表(詳見下文用法) |
|
|
100
|
-
| `pickFolder(title?)` |
|
|
100
|
+
| `pickFolder(title?)` | 呼叫原生資料夾選擇對話方塊(同步阻塞目前呼叫執行緒;**經 IPC 暴露時務必搭配 `timeoutMs: 0`**,見下文) |
|
|
101
101
|
| `libInfo` | 原生動態連結程式庫解析中繼資料(包含實際路徑、載入來源、SHA-256 雜湊值) |
|
|
102
102
|
| `symbols` | 底層直通介面:直接存取透過 `dlopen` 匯出的底層 C 符號 |
|
|
103
103
|
|
|
@@ -252,6 +252,28 @@ api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
|
252
252
|
|
|
253
253
|
預設情況下,`connect` 會從目前頁面 URL 的查詢參數 `?t=` 中讀取驗證 Token 並連線至 `/ws`。若需自訂連線方式,可明確傳入 `url` 或自訂 `channel` 實例。
|
|
254
254
|
|
|
255
|
+
#### `timeoutMs` —— 長時人機互動必須明確關閉
|
|
256
|
+
|
|
257
|
+
單次呼叫預設 **30 秒**逾時。這對一般方法合理,對**人機互動**卻是致命的:
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
// 服務端:pickFolder 內部是 SHBrowseForFolder,同步阻塞、耗時完全由使用者決定
|
|
261
|
+
const service = { pickFolder: () => shell.pickFolder() };
|
|
262
|
+
|
|
263
|
+
// 頁面端:必須關閉逾時,否則使用者挑久一點,用戶端先 reject,而服務端還在正常阻塞
|
|
264
|
+
const api = await connect<Api, Events>({ timeoutMs: 0 });
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`0` 表示**不逾時**(不是「0 毫秒逾時」)。踩坑的形態有兩種,都不會讓門禁變紅:
|
|
268
|
+
|
|
269
|
+
- 頁面沒接 `.catch` → 拒絕完全靜默,表現為「選完了介面毫無反應」。
|
|
270
|
+
- 使用者按了「取消」很快返回,所以只有**挑久了**才重現,容易被判成偶發。
|
|
271
|
+
|
|
272
|
+
還有一層要知道:`shell.pickFolder()` **阻塞的是 Bun 主執行緒**。對話方塊開啟期間,
|
|
273
|
+
同一行程裡的計時器不會觸發,其他 IPC 呼叫也會排隊(不是 reject,是沒有回應)。
|
|
274
|
+
用戶端那 30 秒計時器跑在瀏覽器渲染行程裡,不受影響——所以調短逾時並不能解決阻塞,
|
|
275
|
+
只能讓症狀更早暴露。需要併發就得把 `pickFolder` 放進 Worker 裡呼叫。
|
|
276
|
+
|
|
255
277
|
#### 用戶端保留名稱
|
|
256
278
|
|
|
257
279
|
用戶端是一個 `Proxy`:有幾個名稱會解析到用戶端自己身上,永遠到不了遠端方法。服務表裡若有
|
|
@@ -592,7 +614,7 @@ bun run demo # 啟動互動式自我檢測範例程式(需於桌面工
|
|
|
592
614
|
## 致謝
|
|
593
615
|
|
|
594
616
|
- 底層跨平台視窗抽象源自 [webview/webview](https://github.com/webview/webview) `0.12.0`(MIT 授權,作者 Serge Zaitsev · Steffen André Langnes)。儲存庫內 `vendor/webview/` 目錄為其原始碼原樣拷貝,未作任何侵入式修改。
|
|
595
|
-
- WebView2 C/C++ SDK 源自 [Microsoft.Web.WebView2](https://www.nuget.org/packages/Microsoft.Web.WebView2) `1.0.
|
|
617
|
+
- WebView2 C/C++ SDK 源自 [Microsoft.Web.WebView2](https://www.nuget.org/packages/Microsoft.Web.WebView2) `1.0.4258.31`(NuGet 套件隨儲存庫發佈以支援離線建構),執行環境由作業系統提供。
|
|
596
618
|
- Co-authored-by DeepSeek
|
|
597
619
|
|
|
598
620
|
## 授權條款
|
package/dist/libshell.dll
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "bun-app-window",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.5.0",
|
|
5
5
|
"description": "Native WebView2 window shell for Bun — frameless or system-decorated windows, DPI-aware geometry, typed IPC, system-accurate title bar",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|
package/src/shell/app.ts
CHANGED
|
@@ -326,7 +326,24 @@ export async function createApp(opts: AppOptions): Promise<App> {
|
|
|
326
326
|
// 等 `EVENT_CLOSED` 而不是轮询 `isAlive()`:CLOSED 由原生侧在 teardown
|
|
327
327
|
// 全部做完、g_webview 已置空之后写进队列,它出现的时刻就等于「单例已释放」。
|
|
328
328
|
// 超时上限保留,且超时后也 resolve 一次——否则 `wait()` 会永远挂着。
|
|
329
|
-
|
|
329
|
+
//
|
|
330
|
+
// 定时器必须可取消:`Promise.race` 只取先兑现的,**不取消输掉的那个**。
|
|
331
|
+
// 早先这里写的是 `Bun.sleep(CLOSE_TIMEOUT_MS)`,正常关窗路径下它就变成一个
|
|
332
|
+
// 没人等的、未 unref 的 5 秒定时器,把 Bun 的事件循环撑满——实测
|
|
333
|
+
// `race returned @ 9ms` 之后进程到 5010 ms 才退出,真实关窗 5043 ms。
|
|
334
|
+
// 不用 `Bun.sleep` + `AbortSignal`:Bun 当前版本不支持。
|
|
335
|
+
let cancelTimeout: () => void = () => {};
|
|
336
|
+
const timeout = new Promise<void>((resolve) => {
|
|
337
|
+
const timer = setTimeout(resolve, CLOSE_TIMEOUT_MS);
|
|
338
|
+
cancelTimeout = () => clearTimeout(timer);
|
|
339
|
+
});
|
|
340
|
+
try {
|
|
341
|
+
await Promise.race([closed.promise, timeout]);
|
|
342
|
+
} finally {
|
|
343
|
+
// 判据要落在「进程何时退出」,不是「stop() 何时返回」:
|
|
344
|
+
// 摘掉这一行时 stop() 仍然 42 ms 就返回了,但进程要 5.9 s 才退。
|
|
345
|
+
cancelTimeout();
|
|
346
|
+
}
|
|
330
347
|
closed.resolve();
|
|
331
348
|
events.stop();
|
|
332
349
|
server.stop(true);
|