bun-app-window 0.4.0 → 0.4.1
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 +44 -1
- package/README.md +79 -32
- package/README.zh-CN.md +74 -31
- package/README.zh-TW.md +74 -31
- package/package.json +1 -1
- package/src/ui/titlebar.ts +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,48 @@ 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.4.1] - 2026-10-21
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`bun-app-window/titlebar` no longer breaks `bun check` in your project.** Every consumer whose `tsconfig.json` includes `DOM` — that is, every project with a real page — was stopped by a `TS2352` error pointing at this package's own source, with no way to work around it:
|
|
12
|
+
|
|
13
|
+
```text
|
|
14
|
+
node_modules/bun-app-window/src/ui/titlebar.ts(237,27): error TS2352:
|
|
15
|
+
Conversion of type 'typeof globalThis' to type '{ document?: TitleBarDocument; }'
|
|
16
|
+
may be a mistake because neither type sufficiently overlaps with the other.
|
|
17
|
+
If this was intentional, convert the expression to 'unknown' first.
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`skipLibCheck: true` does not help, because this package ships `.ts` sources rather than `.d.ts` files, so the error is in compiled source and not in a declaration. The component reached `globalThis.document` through a single-layer type assertion; with `DOM` loaded, `globalThis.document` is a full `Document`, and the component's deliberately minimal `TitleBarDocument` does not overlap with it enough to satisfy the checker. It is now a two-layer assertion (`as unknown as`), which states the intent without making the component depend on DOM types. Runtime behaviour is unchanged. Existing code needs no edits — upgrading is enough.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **The three READMEs are corrected in six places.** Everything below was documentation, not code, but each item could cost a consumer real time by making them follow an example that does not work. The pages are `README.md`, `README.zh-CN.md` and `README.zh-TW.md`, and all six corrections are applied to all three.
|
|
25
|
+
|
|
26
|
+
**The `titlebar` example contradicted itself.** One section showed the IPC service flat, another showed it nested under a `window` key — inside the same file. The nested form never worked: `service` is a flat method table. The example now uses the flat form, and because `close` is one of the client's reserved names, it exposes `winClose` and maps it back on the page side.
|
|
27
|
+
|
|
28
|
+
**`debug.metrics()` and `debug.dpi()` were described as if they returned objects.** They return formatted text — `pos=196,196 win=1960x1400 client=1960x1400 work=3072x1824 border=13x13 round=1` and `awareness=-1 dpi=192 scale=200%`. The example shows the real output now, and says not to build a parser on it: the layout is not part of any contract.
|
|
29
|
+
|
|
30
|
+
**`await app.wait()` was shown as the end of a program.** It resolves when the window closes but stops neither the event pump nor the server, and the pump's timer is pinned on purpose — so a program that ends on that line stays alive after the window is gone. The example follows it with `await app.stop()`, which is idempotent and returns immediately once the close has been observed.
|
|
31
|
+
|
|
32
|
+
**The IPC client's reserved names were undocumented.** `on`, `off`, `close`, `connected`, `pending` and `then` resolve to the client itself and never reach a remote method. A service that defines one of them is silently unreachable, with no error on either end — and `close` in particular also tears down the channel for every later call. There is a table of them now, and a note that symbol keys are reserved as well.
|
|
33
|
+
|
|
34
|
+
**Two traps that pass type checking but do not work** are documented together: the page bundle is an ES module, so a classic `<script>` tag executes nothing while the page still renders; and the title bar listens for `document` clicks on `data-act`, so a host binding `button[data-act]` receives every click twice.
|
|
35
|
+
|
|
36
|
+
**The tarball figures were stale** — 19 files / ~0.36 MB against the measured 21 files / ~0.4 MB.
|
|
37
|
+
|
|
38
|
+
- **The `app` Quick Start no longer reads the HTML at run time.** The old snippet, `import html from "./index.html" with { type: "text" }`, did not compile under TypeScript 7.0.2, because `bun-types` gates the declaration that types this import behind `typesVersions: ">= 7.1"`. The obvious-looking repair, `await Bun.file(...).text()`, would have broken the property that section exists to demonstrate: a compile-time constant travels inside `bun build --compile`, a run-time read does not. The import stays and only the type is bridged, so a single-file executable still carries its HTML:
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import htmlText from "./index.html" with { type: "text" };
|
|
42
|
+
const html = htmlText as unknown as string;
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Note
|
|
46
|
+
|
|
47
|
+
- **Nothing to do on upgrade.** No public API changed: the exported surface, the flags, the IPC protocol and `dist/libshell.dll` are byte-for-byte the same as 0.4.0, so `bun update bun-app-window` is the whole procedure. This is a patch because the only code change is the one assertion above, and the rest is documentation.
|
|
48
|
+
|
|
7
49
|
## [0.4.0] - 2026-10-21
|
|
8
50
|
|
|
9
51
|
### Added
|
|
@@ -285,7 +327,8 @@ First public release.
|
|
|
285
327
|
- Not yet verified in managed environments that enforce AppLocker, WDAC, or third-party EDR behaviour blocking.
|
|
286
328
|
- No commercially trusted code signing: only a self-signed certificate has been exercised end to end.
|
|
287
329
|
|
|
288
|
-
[Unreleased]: https://github.com/TiancongLx/bun-app-window/compare/v0.4.
|
|
330
|
+
[Unreleased]: https://github.com/TiancongLx/bun-app-window/compare/v0.4.1...HEAD
|
|
331
|
+
[0.4.1]: https://github.com/TiancongLx/bun-app-window/compare/v0.4.0...v0.4.1
|
|
289
332
|
[0.4.0]: https://github.com/TiancongLx/bun-app-window/compare/v0.3.0...v0.4.0
|
|
290
333
|
[0.3.0]: https://github.com/TiancongLx/bun-app-window/compare/v0.2.2...v0.3.0
|
|
291
334
|
[0.2.2]: https://github.com/TiancongLx/bun-app-window/compare/v0.2.1...v0.2.2
|
package/README.md
CHANGED
|
@@ -46,7 +46,7 @@ bun add bun-app-window
|
|
|
46
46
|
|
|
47
47
|
### What the tarball contains
|
|
48
48
|
|
|
49
|
-
The published package holds the prebuilt `dist/libshell.dll` plus the TypeScript sources —
|
|
49
|
+
The published package holds the prebuilt `dist/libshell.dll` plus the TypeScript sources — 21 files, roughly 0.4 MB packed. The native C++ layer (`native/`) and the vendored upstream headers (`vendor/`) are **not** shipped: they are only needed to rebuild `libshell.dll`, and rebuilding requires cloning the repository. `bun run build:native` and `build:exe` are therefore repository-only scripts that do nothing useful inside `node_modules`.
|
|
50
50
|
|
|
51
51
|
## Quick Start
|
|
52
52
|
|
|
@@ -153,7 +153,12 @@ start hits "already started"). `app` collapses all of it into one call:
|
|
|
153
153
|
// main.ts
|
|
154
154
|
import { createApp } from "bun-app-window/app";
|
|
155
155
|
import { BROWSER_UI_ALL, FLAG_FRAMELESS } from "bun-app-window";
|
|
156
|
-
import
|
|
156
|
+
import htmlText from "./index.html" with { type: "text" };
|
|
157
|
+
|
|
158
|
+
// `with { type: "text" }` inlines the file at build time, so it survives `bun build --compile`.
|
|
159
|
+
// TS 7.0.2 cannot see that declaration — bun-types gates it behind `typesVersions >= 7.1`,
|
|
160
|
+
// so the import resolves to `HTMLBundle` instead of `string`. Hence the bridge.
|
|
161
|
+
const html = htmlText as unknown as string;
|
|
157
162
|
|
|
158
163
|
const app = await createApp({
|
|
159
164
|
html,
|
|
@@ -167,6 +172,7 @@ const app = await createApp({
|
|
|
167
172
|
});
|
|
168
173
|
|
|
169
174
|
await app.wait(); // resolves when the window closes
|
|
175
|
+
await app.stop(); // releases the event pump and the server; idempotent
|
|
170
176
|
```
|
|
171
177
|
|
|
172
178
|
```ts
|
|
@@ -195,10 +201,18 @@ Notes:
|
|
|
195
201
|
- **Does not default to `FLAG_FRAMELESS`.** A frameless window has no system title bar, so it cannot
|
|
196
202
|
be dragged until the page mounts `mountTitleBar`. The default is the side that works even when the
|
|
197
203
|
page does nothing.
|
|
204
|
+
- **`wait()` only waits.** It resolves on `EVENT_CLOSED` but does not stop the event pump or the
|
|
205
|
+
server — and the pump timer is ref'd (`keepAlive: true`), so a process that stops at `wait()`
|
|
206
|
+
keeps running after the window is gone. Always follow it with `stop()`: it is idempotent and
|
|
207
|
+
returns immediately once the close has already been observed.
|
|
198
208
|
- **Static assets are inline strings only.** `with { type: "text" }` yields a compile-time constant:
|
|
199
|
-
nothing touches the filesystem, no temp files, and it survives `bun build --compile`.
|
|
200
|
-
|
|
201
|
-
|
|
209
|
+
nothing touches the filesystem, no temp files, and it survives `bun build --compile`. TS 7.0.2
|
|
210
|
+
resolves that import to `HTMLBundle` rather than `string` — `bun-types` gates the correct
|
|
211
|
+
declaration behind `typesVersions >= 7.1` — which is what the `as unknown as string` in the
|
|
212
|
+
example is for. Do not "fix" it with `Bun.file().text()`: that reads at run time, and the HTML
|
|
213
|
+
would no longer travel inside the compiled executable. For anything read from disk, write a
|
|
214
|
+
`routes` function — five lines, and the path-resolution problem stays yours instead of leaking
|
|
215
|
+
into the skeleton.
|
|
202
216
|
- **Not re-exported from the main entry.** It is a subpath like `./state` and `./ipc`. It imports
|
|
203
217
|
`openShell` from `./index`; exporting it from `index.ts` would make that a cycle.
|
|
204
218
|
|
|
@@ -249,39 +263,59 @@ api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
|
249
263
|
|
|
250
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.
|
|
251
265
|
|
|
266
|
+
#### Reserved client names
|
|
267
|
+
|
|
268
|
+
The client is a `Proxy`: a handful of names resolve to the client itself and never reach a remote
|
|
269
|
+
method. If your service exposes a method under one of these names, **the server-side method becomes
|
|
270
|
+
unreachable** — the page calls the client's own member instead, and neither end reports an error.
|
|
271
|
+
|
|
272
|
+
| Name | What the page actually gets |
|
|
273
|
+
| --- | --- |
|
|
274
|
+
| `on` / `off` | Event subscription helpers |
|
|
275
|
+
| `close()` | Closes the WebSocket channel — every later call is rejected |
|
|
276
|
+
| `connected` | `boolean`: whether the channel is open |
|
|
277
|
+
| `pending` | `number` of calls still awaiting a reply |
|
|
278
|
+
| `then` | `undefined`, so `await client` cannot hang on a never-resolving thenable |
|
|
279
|
+
|
|
280
|
+
Symbol keys are reserved as well. Pick a different service name (`winClose` instead of `close`) and
|
|
281
|
+
map it back on the page side — see the title bar section below.
|
|
282
|
+
|
|
252
283
|
### `bun-app-window/titlebar` — Windows 11 Title Bar
|
|
253
284
|
|
|
254
|
-
Mounts a customizable Windows 11 title bar inside frameless windows
|
|
285
|
+
Mounts a customizable Windows 11 title bar inside frameless windows. The IPC service is **flat** —
|
|
286
|
+
there is no `window` namespace — and one of the four natural names is taken by the client itself,
|
|
287
|
+
so the host exposes `winClose` and the page maps it back:
|
|
255
288
|
|
|
256
289
|
```ts
|
|
257
|
-
//
|
|
290
|
+
// Host — flat service; `close` is reserved by the IPC client
|
|
291
|
+
createIpcServer({
|
|
292
|
+
service: {
|
|
293
|
+
minimize: () => shell.minimize(),
|
|
294
|
+
toggleMaximize: () => shell.toggleMaximize(),
|
|
295
|
+
winClose: () => shell.close(),
|
|
296
|
+
drag: () => shell.drag(),
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
```ts
|
|
302
|
+
// Page — build the bridge explicitly
|
|
258
303
|
import { mountTitleBar, bridgeFromWindowService } from "bun-app-window/titlebar";
|
|
259
304
|
|
|
260
305
|
const bar = mountTitleBar({
|
|
261
306
|
title: "My App",
|
|
262
307
|
icon: "/icon.svg",
|
|
263
|
-
bridge: bridgeFromWindowService(
|
|
308
|
+
bridge: bridgeFromWindowService({
|
|
309
|
+
minimize: () => api.minimize(),
|
|
310
|
+
toggleMaximize: () => api.toggleMaximize(),
|
|
311
|
+
close: () => api.winClose(), // mapped back to the bridge's own name
|
|
312
|
+
drag: () => api.drag(),
|
|
313
|
+
}),
|
|
264
314
|
});
|
|
265
315
|
|
|
266
316
|
api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
267
317
|
```
|
|
268
318
|
|
|
269
|
-
Expose the four corresponding window control methods on the host IPC service:
|
|
270
|
-
|
|
271
|
-
```ts
|
|
272
|
-
// Host
|
|
273
|
-
createIpcServer({
|
|
274
|
-
service: {
|
|
275
|
-
window: {
|
|
276
|
-
minimize: () => shell.minimize(),
|
|
277
|
-
toggleMaximize: () => shell.toggleMaximize(),
|
|
278
|
-
close: () => shell.close(),
|
|
279
|
-
drag: () => shell.drag(),
|
|
280
|
-
},
|
|
281
|
-
},
|
|
282
|
-
});
|
|
283
|
-
```
|
|
284
|
-
|
|
285
319
|
Implementation notes:
|
|
286
320
|
|
|
287
321
|
- Native dragging: Calling `shell.drag()` dispatches a native `WM_NCLBUTTONDOWN` (`HTCAPTION`) message, preserving Windows 11 Snap Layouts and double-click maximize behavior.
|
|
@@ -375,11 +409,14 @@ Exposes internal metrics and hit-testing utilities. Note: This module is exclude
|
|
|
375
409
|
import { debugOf } from "bun-app-window/debug";
|
|
376
410
|
|
|
377
411
|
const d = debugOf(shell);
|
|
378
|
-
d.metrics(); //
|
|
379
|
-
d.dpi(); //
|
|
412
|
+
d.metrics(); // → "pos=196,196 win=1960x1400 client=1960x1400 work=3072x1824 border=13x13 round=1"
|
|
413
|
+
d.dpi(); // → "awareness=-1 dpi=192 scale=200%"
|
|
380
414
|
d.hittest(x, y);
|
|
381
415
|
```
|
|
382
416
|
|
|
417
|
+
`metrics()` and `dpi()` return **formatted text**, not objects. Parse it in throwaway tooling if you
|
|
418
|
+
must; do not put a parser in production code, because the layout is not part of any contract.
|
|
419
|
+
|
|
383
420
|
### `bun-app-window/native` — The Prebuilt DLL
|
|
384
421
|
|
|
385
422
|
Resolves to `dist/libshell.dll` itself, so `import.meta.resolve()` hands you a real path you can copy next to your executable:
|
|
@@ -421,13 +458,10 @@ Note: `F11`, `F12`, and `Ctrl±` zoom shortcuts are handled directly by the nati
|
|
|
421
458
|
After disabling the default browser menu, render native Win32 popup menus (`TrackPopupMenu`) via `showContextMenu`. The page cannot call it directly — expose it through the IPC service first:
|
|
422
459
|
|
|
423
460
|
```ts
|
|
424
|
-
// Host: expose the menu capability on the
|
|
461
|
+
// Host: expose the menu capability on the flat service
|
|
425
462
|
createIpcServer({
|
|
426
463
|
service: {
|
|
427
|
-
|
|
428
|
-
showContextMenu: (x: number, y: number, items: string) =>
|
|
429
|
-
shell.showContextMenu(x, y, items),
|
|
430
|
-
},
|
|
464
|
+
showContextMenu: (x: number, y: number, items: string) => shell.showContextMenu(x, y, items),
|
|
431
465
|
},
|
|
432
466
|
});
|
|
433
467
|
```
|
|
@@ -436,7 +470,7 @@ createIpcServer({
|
|
|
436
470
|
// Page: trigger the native menu
|
|
437
471
|
document.addEventListener("contextmenu", async (e) => {
|
|
438
472
|
e.preventDefault();
|
|
439
|
-
await api.
|
|
473
|
+
await api.showContextMenu(
|
|
440
474
|
Math.round(e.screenX),
|
|
441
475
|
Math.round(e.screenY),
|
|
442
476
|
"open|Open\nrefresh|Refresh\n-\nquit|Quit"
|
|
@@ -508,6 +542,19 @@ Tips:
|
|
|
508
542
|
- A side-effect import `import "bun-app-window/embedded";` also works automatically.
|
|
509
543
|
- Always pass `--windows-hide-console` for GUI builds; omitting it will spawn a black command prompt window alongside your application.
|
|
510
544
|
|
|
545
|
+
### 6. Two Traps That Type-Check
|
|
546
|
+
|
|
547
|
+
Neither of these fails `bun check`; both produce a window that renders and then does nothing.
|
|
548
|
+
|
|
549
|
+
- **The page bundle is an ES module, so the `<script>` tag needs `type="module"`.** A build with a
|
|
550
|
+
top-level `await` is a syntax error inside a classic `<script>`, and the **whole script never
|
|
551
|
+
runs** — while the page still renders, because the markup is static. Nothing on screen points at
|
|
552
|
+
the cause.
|
|
553
|
+
- **Namespace your own `data-*` selectors.** The title bar renders buttons with
|
|
554
|
+
`data-act="minimize|maximize|close"` and listens for clicks on `document`. A host that also binds
|
|
555
|
+
`button[data-act]` receives the same click twice and logs its own "unknown action: maximize".
|
|
556
|
+
Prefix your attributes (`data-mvp-act`): a generic name is a shared namespace, not yours.
|
|
557
|
+
|
|
511
558
|
## Internals & Reference
|
|
512
559
|
|
|
513
560
|
### Native Library Resolution Order
|
package/README.zh-CN.md
CHANGED
|
@@ -46,7 +46,7 @@ bun add bun-app-window
|
|
|
46
46
|
|
|
47
47
|
### 包内包含什么
|
|
48
48
|
|
|
49
|
-
发布的 tarball 只有预编译的 `dist/libshell.dll` 与 TypeScript 源码,共
|
|
49
|
+
发布的 tarball 只有预编译的 `dist/libshell.dll` 与 TypeScript 源码,共 21 个文件(约 0.4 MB)。原生 C++ 层(`native/`)与上游 vendored 头文件(`vendor/`)**不随包分发**:它们只在重编 `libshell.dll` 时需要,而重编必须 clone 仓库。因此 `bun run build:native` 与 `build:exe` 是本仓库专用脚本,装进 `node_modules` 后执行没有意义。
|
|
50
50
|
|
|
51
51
|
## 快速开始
|
|
52
52
|
|
|
@@ -150,7 +150,12 @@ shell.setTitle("Doing work…");
|
|
|
150
150
|
// main.ts
|
|
151
151
|
import { createApp } from "bun-app-window/app";
|
|
152
152
|
import { BROWSER_UI_ALL, FLAG_FRAMELESS } from "bun-app-window";
|
|
153
|
-
import
|
|
153
|
+
import htmlText from "./index.html" with { type: "text" };
|
|
154
|
+
|
|
155
|
+
// `with { type: "text" }` 让 Bun 在打包期把文件内联进去,所以 `bun build --compile` 后仍然成立。
|
|
156
|
+
// TS 7.0.2 看不到这条声明——bun-types 把它 gate 在 `typesVersions >= 7.1` 后面,
|
|
157
|
+
// 于是这个 import 被解析成 `HTMLBundle` 而不是 `string`。桥梁就落在这里。
|
|
158
|
+
const html = htmlText as unknown as string;
|
|
154
159
|
|
|
155
160
|
const app = await createApp({
|
|
156
161
|
html,
|
|
@@ -164,6 +169,7 @@ const app = await createApp({
|
|
|
164
169
|
});
|
|
165
170
|
|
|
166
171
|
await app.wait(); // 窗口关闭时 resolve
|
|
172
|
+
await app.stop(); // 释放事件泵与服务;幂等,已关闭时立即返回
|
|
167
173
|
```
|
|
168
174
|
|
|
169
175
|
```ts
|
|
@@ -188,9 +194,15 @@ await api.greet("Ada");
|
|
|
188
194
|
|
|
189
195
|
- **默认不带 `FLAG_FRAMELESS`。** 无边框窗口没有系统标题栏就拖不动,必须由页面先挂上
|
|
190
196
|
`mountTitleBar`。默认值选的是「页面什么都不做也能用」的那一侧。
|
|
197
|
+
- **`wait()` 只负责等。** 它在 `EVENT_CLOSED` 上 resolve,但不会停事件泵、也不会关服务——而
|
|
198
|
+
事件泵的定时器是 ref 的(`keepAlive: true`),所以停在这一行的进程会在窗口消失后继续挂着。
|
|
199
|
+
必须跟一句 `stop()`:它幂等,且关闭已被观察到时会立即返回。
|
|
191
200
|
- **静态资源只吃内联字符串。** `with { type: "text" }` 得到的是编译期常量:不碰文件系统、
|
|
192
|
-
不产生临时文件、`bun build --compile`
|
|
193
|
-
|
|
201
|
+
不产生临时文件、`bun build --compile` 后仍然成立。TS 7.0.2 会把这条 import 解析成
|
|
202
|
+
`HTMLBundle` 而不是 `string`(`bun-types` 把正确的声明 gate 在 `typesVersions >= 7.1`
|
|
203
|
+
后面),示例里的 `as unknown as string` 就是为此而写。别用 `Bun.file().text()` 去「修」它:
|
|
204
|
+
那是运行期读盘,HTML 就不在编译产物里了。要从磁盘读东西就写个 `routes` 函数 —— 五行,
|
|
205
|
+
而且路径解析的问题留在你这边,不会渗进骨架。
|
|
194
206
|
- **不从主入口导出。** 它和 `./state`、`./ipc` 一样是子路径。它从 `./index` 导入
|
|
195
207
|
`openShell`;若 `index.ts` 反过来导出它,就成了循环。
|
|
196
208
|
|
|
@@ -240,39 +252,58 @@ api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
|
240
252
|
|
|
241
253
|
默认情况下,`connect` 会从当前页面 URL 的查询参数 `?t=` 中读取鉴权 Token 并连接至 `/ws`。若需自定义连接方式,可显式传入 `url` 或自定义 `channel` 实例。
|
|
242
254
|
|
|
255
|
+
#### 客户端保留名
|
|
256
|
+
|
|
257
|
+
客户端是一个 `Proxy`:有几个名字会解析到客户端自己身上,永远到不了远端方法。服务表里若有
|
|
258
|
+
同名方法,**那个服务端方法就永远调不到**——页面调的是客户端自己的成员,两端都不会报错。
|
|
259
|
+
|
|
260
|
+
| 名字 | 页面实际拿到的东西 |
|
|
261
|
+
| --- | --- |
|
|
262
|
+
| `on` / `off` | 事件订阅辅助函数 |
|
|
263
|
+
| `close()` | 关闭 WebSocket 通道——之后所有调用都会被 reject |
|
|
264
|
+
| `connected` | `boolean`,通道当前是否可用 |
|
|
265
|
+
| `pending` | `number`,尚未收到回复的调用数 |
|
|
266
|
+
| `then` | `undefined`,否则 `await client` 会把这个永不兑现的 thenable 等下去 |
|
|
267
|
+
|
|
268
|
+
Symbol 键同样保留。给服务方法换个名字(`winClose` 而不是 `close`),在页面侧映射回去 ——
|
|
269
|
+
见下面的标题栏一节。
|
|
270
|
+
|
|
243
271
|
### `bun-app-window/titlebar` — Windows 11 风格标题栏
|
|
244
272
|
|
|
245
|
-
用于在无边框模式下快速挂载符合 Windows 11
|
|
273
|
+
用于在无边框模式下快速挂载符合 Windows 11 视觉规范的自定义标题栏。IPC 服务是**扁平的**——
|
|
274
|
+
没有 `window` 这一层命名空间——而且四个最自然的名字里有一个被客户端自己占了,所以宿主端
|
|
275
|
+
暴露 `winClose`,页面侧再映射回去:
|
|
246
276
|
|
|
247
277
|
```ts
|
|
248
|
-
//
|
|
278
|
+
// 宿主端 —— 扁平服务;`close` 是 IPC 客户端的保留名
|
|
279
|
+
createIpcServer({
|
|
280
|
+
service: {
|
|
281
|
+
minimize: () => shell.minimize(),
|
|
282
|
+
toggleMaximize: () => shell.toggleMaximize(),
|
|
283
|
+
winClose: () => shell.close(),
|
|
284
|
+
drag: () => shell.drag(),
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
// 页面端 —— 显式拼出 bridge
|
|
249
291
|
import { mountTitleBar, bridgeFromWindowService } from "bun-app-window/titlebar";
|
|
250
292
|
|
|
251
293
|
const bar = mountTitleBar({
|
|
252
294
|
title: "我的应用",
|
|
253
295
|
icon: "/icon.svg",
|
|
254
|
-
bridge: bridgeFromWindowService(
|
|
296
|
+
bridge: bridgeFromWindowService({
|
|
297
|
+
minimize: () => api.minimize(),
|
|
298
|
+
toggleMaximize: () => api.toggleMaximize(),
|
|
299
|
+
close: () => api.winClose(), // 桥接时再映射回 close
|
|
300
|
+
drag: () => api.drag(),
|
|
301
|
+
}),
|
|
255
302
|
});
|
|
256
303
|
|
|
257
304
|
api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
258
305
|
```
|
|
259
306
|
|
|
260
|
-
宿主端需在 IPC 服务中暴露对应的四个窗口控制方法:
|
|
261
|
-
|
|
262
|
-
```ts
|
|
263
|
-
// 宿主端
|
|
264
|
-
createIpcServer({
|
|
265
|
-
service: {
|
|
266
|
-
window: {
|
|
267
|
-
minimize: () => shell.minimize(),
|
|
268
|
-
toggleMaximize: () => shell.toggleMaximize(),
|
|
269
|
-
close: () => shell.close(),
|
|
270
|
-
drag: () => shell.drag(),
|
|
271
|
-
},
|
|
272
|
-
},
|
|
273
|
-
});
|
|
274
|
-
```
|
|
275
|
-
|
|
276
307
|
实现细节:
|
|
277
308
|
|
|
278
309
|
- 原生拖拽体验:拖拽操作通过调用 `shell.drag()` 交由底层发送 `WM_NCLBUTTONDOWN`(`HTCAPTION`)消息处理,完整保留 Windows 11 贴边分屏(Snap Layouts)与双击最大化等系统级行为。
|
|
@@ -358,11 +389,14 @@ pump.onGap((missing) => console.warn(`漏了 ${missing} 条事件`));
|
|
|
358
389
|
import { debugOf } from "bun-app-window/debug";
|
|
359
390
|
|
|
360
391
|
const d = debugOf(shell);
|
|
361
|
-
d.metrics(); //
|
|
362
|
-
d.dpi(); //
|
|
392
|
+
d.metrics(); // → "pos=196,196 win=1960x1400 client=1960x1400 work=3072x1824 border=13x13 round=1"
|
|
393
|
+
d.dpi(); // → "awareness=-1 dpi=192 scale=200%"
|
|
363
394
|
d.hittest(x, y);
|
|
364
395
|
```
|
|
365
396
|
|
|
397
|
+
`metrics()` 与 `dpi()` 返回的是**格式化文本**,不是对象。真要解析就写进一次性脚本里,别把解析器
|
|
398
|
+
塞进生产代码——这个格式不属于任何契约。
|
|
399
|
+
|
|
366
400
|
### `bun-app-window/native` — 预编译动态库本体
|
|
367
401
|
|
|
368
402
|
该子路径解析到 `dist/libshell.dll` 本身,`import.meta.resolve()` 拿到的是可直接拷到 exe 旁边的真实路径:
|
|
@@ -404,13 +438,10 @@ const shell = await openShell({ url, browserUi: BROWSER_UI_ALL });
|
|
|
404
438
|
禁用默认浏览器菜单后,可通过 `showContextMenu` 唤起基于 Win32 `TrackPopupMenu` 绘制的原生菜单。页面无法直接调用它——需要先在 IPC 服务上暴露:
|
|
405
439
|
|
|
406
440
|
```ts
|
|
407
|
-
//
|
|
441
|
+
// 宿主端:把菜单能力挂到扁平服务上
|
|
408
442
|
createIpcServer({
|
|
409
443
|
service: {
|
|
410
|
-
|
|
411
|
-
showContextMenu: (x: number, y: number, items: string) =>
|
|
412
|
-
shell.showContextMenu(x, y, items),
|
|
413
|
-
},
|
|
444
|
+
showContextMenu: (x: number, y: number, items: string) => shell.showContextMenu(x, y, items),
|
|
414
445
|
},
|
|
415
446
|
});
|
|
416
447
|
```
|
|
@@ -419,7 +450,7 @@ createIpcServer({
|
|
|
419
450
|
// 页面端触发菜单
|
|
420
451
|
document.addEventListener("contextmenu", async (e) => {
|
|
421
452
|
e.preventDefault();
|
|
422
|
-
await api.
|
|
453
|
+
await api.showContextMenu(
|
|
423
454
|
Math.round(e.screenX),
|
|
424
455
|
Math.round(e.screenY),
|
|
425
456
|
"open|打开\nrefresh|刷新\n-\nquit|退出"
|
|
@@ -489,6 +520,18 @@ bun build --compile --windows-hide-console src/index.ts --outfile dist/app.exe
|
|
|
489
520
|
- 仅通过副作用导入 `import "bun-app-window/embedded";` 同样生效。
|
|
490
521
|
- 打包桌面端应用时请务必添加 `--windows-hide-console` 参数,否则启动时将伴随弹出黑色命令行控制台窗口。
|
|
491
522
|
|
|
523
|
+
### 6. 两个类型检查抓不到的坑
|
|
524
|
+
|
|
525
|
+
这两条都不会让 `bun check` 变红,但都会得到一个「渲染正常、什么也不做」的窗口。
|
|
526
|
+
|
|
527
|
+
- **页面 bundle 是 ES 模块,`<script>` 标签必须写 `type="module"`。** 带顶层 `await` 的构建
|
|
528
|
+
产物塞进经典 `<script>` 就是语法错,**整个脚本一行都不执行**——而页面照样渲染,因为 HTML
|
|
529
|
+
是静态的。屏幕上没有任何东西指向这个原因。
|
|
530
|
+
- **自己的 `data-*` 选择器要给命名空间。** 标题栏的按钮用
|
|
531
|
+
`data-act="minimize|maximize|close"`,并在 `document` 上挂 click 监听。宿主如果也绑
|
|
532
|
+
`button[data-act]`,同一次点击会被接两遍,然后打出自己那句「未知动作:maximize」。给自己的
|
|
533
|
+
属性加前缀(`data-mvp-act`)——泛化属性名是共享命名空间,不是你的地盘。
|
|
534
|
+
|
|
492
535
|
## 底层机制与交互参考
|
|
493
536
|
|
|
494
537
|
### 原生动态库加载顺序
|
package/README.zh-TW.md
CHANGED
|
@@ -46,7 +46,7 @@ bun add bun-app-window
|
|
|
46
46
|
|
|
47
47
|
### 套件內容包含什麼
|
|
48
48
|
|
|
49
|
-
發佈的 tarball 只有預先編譯的 `dist/libshell.dll` 與 TypeScript 原始碼,共
|
|
49
|
+
發佈的 tarball 只有預先編譯的 `dist/libshell.dll` 與 TypeScript 原始碼,共 21 個檔案(約 0.4 MB)。原生 C++ 層(`native/`)與上游 vendored 標頭檔(`vendor/`)**不隨套件散布**:它們只在重新編譯 `libshell.dll` 時需要,而重新編譯必須 clone 儲存庫。因此 `bun run build:native` 與 `build:exe` 是本儲存庫專用指令碼,安裝進 `node_modules` 後執行沒有意義。
|
|
50
50
|
|
|
51
51
|
## 快速開始
|
|
52
52
|
|
|
@@ -150,7 +150,12 @@ shell.setTitle("Doing work…");
|
|
|
150
150
|
// main.ts
|
|
151
151
|
import { createApp } from "bun-app-window/app";
|
|
152
152
|
import { BROWSER_UI_ALL, FLAG_FRAMELESS } from "bun-app-window";
|
|
153
|
-
import
|
|
153
|
+
import htmlText from "./index.html" with { type: "text" };
|
|
154
|
+
|
|
155
|
+
// `with { type: "text" }` 讓 Bun 在封裝期把檔案內嵌進去,所以 `bun build --compile` 後仍然成立。
|
|
156
|
+
// TS 7.0.2 看不到這條宣告——bun-types 把它 gate 在 `typesVersions >= 7.1` 之後,
|
|
157
|
+
// 於是這個 import 被解析成 `HTMLBundle` 而不是 `string`。橋樑就落在這裡。
|
|
158
|
+
const html = htmlText as unknown as string;
|
|
154
159
|
|
|
155
160
|
const app = await createApp({
|
|
156
161
|
html,
|
|
@@ -164,6 +169,7 @@ const app = await createApp({
|
|
|
164
169
|
});
|
|
165
170
|
|
|
166
171
|
await app.wait(); // 視窗關閉時 resolve
|
|
172
|
+
await app.stop(); // 釋放事件泵與服務;冪等,已關閉時立即回傳
|
|
167
173
|
```
|
|
168
174
|
|
|
169
175
|
```ts
|
|
@@ -188,9 +194,15 @@ await api.greet("Ada");
|
|
|
188
194
|
|
|
189
195
|
- **預設不帶 `FLAG_FRAMELESS`。** 無邊框視窗沒有系統標題列就拖不動,必須由頁面先掛上
|
|
190
196
|
`mountTitleBar`。預設值選的是「頁面什麼都不做也能用」的那一側。
|
|
197
|
+
- **`wait()` 只負責等。** 它在 `EVENT_CLOSED` 上 resolve,但不會停事件泵、也不會關服務——而
|
|
198
|
+
事件泵的計時器是 ref 的(`keepAlive: true`),所以停在這一行的行程會在視窗消失後繼續掛著。
|
|
199
|
+
必須跟一句 `stop()`:它冪等,且關閉已被觀察到時會立即回傳。
|
|
191
200
|
- **靜態資源只吃內嵌字串。** `with { type: "text" }` 得到的是編譯期常數:不碰檔案系統、
|
|
192
|
-
不產生暫存檔、`bun build --compile`
|
|
193
|
-
|
|
201
|
+
不產生暫存檔、`bun build --compile` 後仍然成立。TS 7.0.2 會把這條 import 解析成
|
|
202
|
+
`HTMLBundle` 而不是 `string`(`bun-types` 把正確的宣告 gate 在 `typesVersions >= 7.1`
|
|
203
|
+
之後),範例裡的 `as unknown as string` 就是為此而寫。別用 `Bun.file().text()` 去「修」它:
|
|
204
|
+
那是執行期讀磁碟,HTML 就不在編譯產物裡了。要從磁碟讀東西就寫個 `routes` 函式 —— 五行,
|
|
205
|
+
而且路徑解析的問題留在你這邊,不會滲進骨架。
|
|
194
206
|
- **不從主入口匯出。** 它和 `./state`、`./ipc` 一樣是子路徑。它從 `./index` 匯入
|
|
195
207
|
`openShell`;若 `index.ts` 反過來匯出它,就成了循環。
|
|
196
208
|
|
|
@@ -240,39 +252,58 @@ api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
|
240
252
|
|
|
241
253
|
預設情況下,`connect` 會從目前頁面 URL 的查詢參數 `?t=` 中讀取驗證 Token 並連線至 `/ws`。若需自訂連線方式,可明確傳入 `url` 或自訂 `channel` 實例。
|
|
242
254
|
|
|
255
|
+
#### 用戶端保留名稱
|
|
256
|
+
|
|
257
|
+
用戶端是一個 `Proxy`:有幾個名稱會解析到用戶端自己身上,永遠到不了遠端方法。服務表裡若有
|
|
258
|
+
同名方法,**那個服務端方法就永遠呼叫不到**——頁面呼叫的是用戶端自己的成員,兩端都不會報錯。
|
|
259
|
+
|
|
260
|
+
| 名稱 | 頁面實際拿到的東西 |
|
|
261
|
+
| --- | --- |
|
|
262
|
+
| `on` / `off` | 事件訂閱輔助函式 |
|
|
263
|
+
| `close()` | 關閉 WebSocket 通道——之後所有呼叫都會被 reject |
|
|
264
|
+
| `connected` | `boolean`,通道目前是否可用 |
|
|
265
|
+
| `pending` | `number`,尚未收到回覆的呼叫數 |
|
|
266
|
+
| `then` | `undefined`,否則 `await client` 會把這個永不兌現的 thenable 等下去 |
|
|
267
|
+
|
|
268
|
+
Symbol 鍵同樣保留。給服務方法換個名稱(`winClose` 而不是 `close`),在頁面端映射回去 ——
|
|
269
|
+
見下方的標題列一節。
|
|
270
|
+
|
|
243
271
|
### `bun-app-window/titlebar` — Windows 11 風格標題列
|
|
244
272
|
|
|
245
|
-
用於在無邊框模式下快速掛載符合 Windows 11
|
|
273
|
+
用於在無邊框模式下快速掛載符合 Windows 11 視覺規範的自訂標題列。IPC 服務是**扁平的**——
|
|
274
|
+
沒有 `window` 這一層命名空間——而且四個最自然的名稱裡有一個被用戶端自己佔了,所以宿主端
|
|
275
|
+
公開 `winClose`,頁面端再映射回去:
|
|
246
276
|
|
|
247
277
|
```ts
|
|
248
|
-
//
|
|
278
|
+
// 宿主端 —— 扁平服務;`close` 是 IPC 用戶端的保留名稱
|
|
279
|
+
createIpcServer({
|
|
280
|
+
service: {
|
|
281
|
+
minimize: () => shell.minimize(),
|
|
282
|
+
toggleMaximize: () => shell.toggleMaximize(),
|
|
283
|
+
winClose: () => shell.close(),
|
|
284
|
+
drag: () => shell.drag(),
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
// 頁面端 —— 明確拼出 bridge
|
|
249
291
|
import { mountTitleBar, bridgeFromWindowService } from "bun-app-window/titlebar";
|
|
250
292
|
|
|
251
293
|
const bar = mountTitleBar({
|
|
252
294
|
title: "我的應用程式",
|
|
253
295
|
icon: "/icon.svg",
|
|
254
|
-
bridge: bridgeFromWindowService(
|
|
296
|
+
bridge: bridgeFromWindowService({
|
|
297
|
+
minimize: () => api.minimize(),
|
|
298
|
+
toggleMaximize: () => api.toggleMaximize(),
|
|
299
|
+
close: () => api.winClose(), // 橋接時再映射回 close
|
|
300
|
+
drag: () => api.drag(),
|
|
301
|
+
}),
|
|
255
302
|
});
|
|
256
303
|
|
|
257
304
|
api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
258
305
|
```
|
|
259
306
|
|
|
260
|
-
宿主端需在 IPC 服務中公開對應的四個視窗控制方法:
|
|
261
|
-
|
|
262
|
-
```ts
|
|
263
|
-
// 宿主端
|
|
264
|
-
createIpcServer({
|
|
265
|
-
service: {
|
|
266
|
-
window: {
|
|
267
|
-
minimize: () => shell.minimize(),
|
|
268
|
-
toggleMaximize: () => shell.toggleMaximize(),
|
|
269
|
-
close: () => shell.close(),
|
|
270
|
-
drag: () => shell.drag(),
|
|
271
|
-
},
|
|
272
|
-
},
|
|
273
|
-
});
|
|
274
|
-
```
|
|
275
|
-
|
|
276
307
|
實作細節:
|
|
277
308
|
|
|
278
309
|
- 原生拖曳體驗:拖曳操作透過呼叫 `shell.drag()` 交由底層發送 `WM_NCLBUTTONDOWN`(`HTCAPTION`)訊息處理,完整保留 Windows 11 貼齊版面配置(Snap Layouts)與連按兩下最大化等系統級行為。
|
|
@@ -359,11 +390,14 @@ pump.onGap((missing) => console.warn(`漏了 ${missing} 條事件`));
|
|
|
359
390
|
import { debugOf } from "bun-app-window/debug";
|
|
360
391
|
|
|
361
392
|
const d = debugOf(shell);
|
|
362
|
-
d.metrics(); //
|
|
363
|
-
d.dpi(); //
|
|
393
|
+
d.metrics(); // → "pos=196,196 win=1960x1400 client=1960x1400 work=3072x1824 border=13x13 round=1"
|
|
394
|
+
d.dpi(); // → "awareness=-1 dpi=192 scale=200%"
|
|
364
395
|
d.hittest(x, y);
|
|
365
396
|
```
|
|
366
397
|
|
|
398
|
+
`metrics()` 與 `dpi()` 回傳的是**格式化文字**,不是物件。真要解析就寫進一次性指令碼裡,別把
|
|
399
|
+
解析器塞進正式程式碼——這個格式不屬於任何契約。
|
|
400
|
+
|
|
367
401
|
### `bun-app-window/native` — 預編譯動態連結程式庫本體
|
|
368
402
|
|
|
369
403
|
該子路徑解析到 `dist/libshell.dll` 本身,`import.meta.resolve()` 拿到的是可直接複製到 exe 旁邊的真實路徑:
|
|
@@ -405,13 +439,10 @@ const shell = await openShell({ url, browserUi: BROWSER_UI_ALL });
|
|
|
405
439
|
停用預設瀏覽器選單後,可透過 `showContextMenu` 喚起基於 Win32 `TrackPopupMenu` 繪製的原生選單。頁面無法直接呼叫它——需先在 IPC 服務上暴露:
|
|
406
440
|
|
|
407
441
|
```ts
|
|
408
|
-
//
|
|
442
|
+
// 宿主端:將選單能力掛載到扁平服務上
|
|
409
443
|
createIpcServer({
|
|
410
444
|
service: {
|
|
411
|
-
|
|
412
|
-
showContextMenu: (x: number, y: number, items: string) =>
|
|
413
|
-
shell.showContextMenu(x, y, items),
|
|
414
|
-
},
|
|
445
|
+
showContextMenu: (x: number, y: number, items: string) => shell.showContextMenu(x, y, items),
|
|
415
446
|
},
|
|
416
447
|
});
|
|
417
448
|
```
|
|
@@ -420,7 +451,7 @@ createIpcServer({
|
|
|
420
451
|
// 頁面端觸發選單
|
|
421
452
|
document.addEventListener("contextmenu", async (e) => {
|
|
422
453
|
e.preventDefault();
|
|
423
|
-
await api.
|
|
454
|
+
await api.showContextMenu(
|
|
424
455
|
Math.round(e.screenX),
|
|
425
456
|
Math.round(e.screenY),
|
|
426
457
|
"open|開啟\nrefresh|重新整理\n-\nquit|結束"
|
|
@@ -490,6 +521,18 @@ bun build --compile --windows-hide-console src/index.ts --outfile dist/app.exe
|
|
|
490
521
|
- 僅透過副作用匯入 `import "bun-app-window/embedded";` 同樣生效。
|
|
491
522
|
- 封裝桌面端應用程式時請務必加上 `--windows-hide-console` 參數,否則啟動時將伴隨彈出黑色主控台(Console)視窗。
|
|
492
523
|
|
|
524
|
+
### 6. 兩個型別檢查抓不到的坑
|
|
525
|
+
|
|
526
|
+
這兩條都不會讓 `bun check` 變紅,但都會得到一個「渲染正常、什麼也不做」的視窗。
|
|
527
|
+
|
|
528
|
+
- **頁面 bundle 是 ES 模組,`<script>` 標籤必須寫 `type="module"`。** 帶頂層 `await` 的封裝
|
|
529
|
+
產物塞進傳統 `<script>` 就是語法錯誤,**整個指令碼一行都不執行**——而頁面照樣渲染,因為
|
|
530
|
+
HTML 是靜態的。螢幕上沒有任何東西指向這個原因。
|
|
531
|
+
- **自己的 `data-*` 選擇器要給命名空間。** 標題列的按鈕用
|
|
532
|
+
`data-act="minimize|maximize|close"`,並在 `document` 上掛 click 監聽。宿主如果也綁
|
|
533
|
+
`button[data-act]`,同一次點擊會被接兩遍,然後打出自己那句「未知動作:maximize」。給自己的
|
|
534
|
+
屬性加前綴(`data-mvp-act`)——泛化屬性名稱是共享命名空間,不是你的地盤。
|
|
535
|
+
|
|
493
536
|
## 底層機制與互動參考
|
|
494
537
|
|
|
495
538
|
### 原生動態連結程式庫載入順序
|
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.
|
|
4
|
+
"version": "0.4.1",
|
|
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/ui/titlebar.ts
CHANGED
|
@@ -234,7 +234,12 @@ export type TitleBarHandle = {
|
|
|
234
234
|
};
|
|
235
235
|
|
|
236
236
|
function resolveDocument(doc?: TitleBarDocument): TitleBarDocument {
|
|
237
|
-
|
|
237
|
+
// 双层断言是必需的,不是笔误:消费者 tsconfig 的 `lib` 含 `DOM` 时,
|
|
238
|
+
// `globalThis.document` 是完整的 `Document`,与本组件的极简结构不重叠,
|
|
239
|
+
// 单层 `as` 会被判成 TS2352「两侧类型不够重叠」。`skipLibCheck` 挡不住——
|
|
240
|
+
// 这是包内 `.ts` 源码,不是 `.d.ts`。本仓 tsconfig 的 lib 只有 ESNext,
|
|
241
|
+
// 这个错在本仓永远复现不出来,判据落在 tests/titlebar.test.ts 的源码断言上。
|
|
242
|
+
const found = doc ?? (globalThis as unknown as { document?: TitleBarDocument }).document;
|
|
238
243
|
if (!found) {
|
|
239
244
|
throw new Error("mountTitleBar: no document available; pass options.document");
|
|
240
245
|
}
|