bun-app-window 0.3.0 → 0.4.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 +67 -2
- package/README.md +126 -4
- package/README.zh-CN.md +111 -4
- package/README.zh-TW.md +112 -4
- package/dist/libshell.dll +0 -0
- package/package.json +3 -1
- package/src/shell/app.ts +344 -0
- package/src/shell/events.ts +297 -0
- package/src/shell/index.ts +37 -0
- package/src/shell/native.ts +4 -2
- package/src/shell/state.ts +115 -21
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,71 @@ 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
|
-
## [
|
|
7
|
+
## [0.4.0] - 2026-10-21
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `bun-app-window/events` — window lifecycle events, and the end of polling. Until this release the host had exactly one way to learn that the window changed: ask again. `shell.isAlive()` every 50 ms to notice a close, and `attachPlacementStore` calling `getBounds()` every 700 ms. The module comment in `state.ts` gave the reason — "the native side pushes window state to the **page** (shell-msg); the host cannot see it, and adding a native→host callback channel means a whole lifecycle to manage".
|
|
12
|
+
|
|
13
|
+
A callback is not available: `webview_run` blocks the native UI thread while `bun:ffi`'s `JSCallback` needs an idle JS thread, and the two together deadlock (ADR 0002). So the channel is a fixed ring plus a pull:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
native: UI thread appends 32-byte records to a 128-slot ring,
|
|
17
|
+
holding one SRWLOCK for at most a 4 KiB copy
|
|
18
|
+
host: shell_poll_events(buf, capacity) → shell.pollEvents() / attachEventPump()
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Four decisions, each with a failure mode behind it:
|
|
22
|
+
|
|
23
|
+
**Capacity 128, drop the oldest.** A one-second drag at a 16 ms cadence is about 60 events, so 128 is a two-fold margin. Dropping the *newest* instead would freeze the ring at a stale state and the host would see "the window never changed" — far worse than losing history. Losses are reported as gaps in `seq` rather than synthesised into an overflow event: the gap carries the exact count, and no new `seq` special case is needed.
|
|
24
|
+
|
|
25
|
+
**`CLOSED` is emitted after teardown completes.** That is the whole point: "saw `CLOSED`" must be equivalent to "the process-wide singleton is released, restart is safe". It follows that `shell_poll_events` must **not** return -1 when `g_webview` is null, unlike every other export — `CLOSED` is produced precisely after the handle is gone.
|
|
26
|
+
|
|
27
|
+
**Both window procedures enqueue.** `shell_wnd_proc` is only registered for the `webview_shell` class, which only `FLAG_FRAMELESS` uses. A framed window is created by the webview library, and only the `toplevel_proc` subclass sees its `WM_SIZE`. Enqueueing next to `push_state()` alone would leave the **default** configuration permanently silent about size and maximize events, with every test green.
|
|
28
|
+
|
|
29
|
+
**The event side keeps its own change-detection cache.** `push_state` compares against `g_pushed` to throttle cross-process `webview_eval`. Recording the same state into the ring is a memory write, and the consumers differ: sharing the cache would let the page push's "unchanged, skip" swallow the host's events too.
|
|
30
|
+
|
|
31
|
+
- `shell.pollEvents(max?)` on the main entry — the raw pull primitive. Synchronous, consuming, and documented as single-consumer: drained records never come back, so a process should have exactly one reader.
|
|
32
|
+
|
|
33
|
+
- `EventCursor` and `attachEventPump()` — the cursor turns `seq` into "how many did I miss", and the pump supplies a cadence with multi-subscriber fan-out (`on()` / `onGap()`), stopping itself once `CLOSED` arrives.
|
|
34
|
+
|
|
35
|
+
- `bun-app-window/app` — an application skeleton, and the answer to a specific number: **180**. That is how much host boilerplate a first app needed before this release, against roughly 30 lines of business code. Picking a browser-safe port, minting a per-run auth token, handling the WebSocket upgrade, serving HTML as a string, wiring `createIpcServer` into Bun's websocket callbacks, composing window flags, handling `openShell` failure, waiting for window close, and calling `stop()` on two things — each piece is easy, but they are only correct in one order. `createApp()` collapses it:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
const app = await createApp({ html, service: { greet: (n) => `hi ${n}` } });
|
|
39
|
+
await app.wait();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Three decisions worth recording:
|
|
43
|
+
|
|
44
|
+
**It does not default to `FLAG_FRAMELESS`.** A frameless window has no system title bar, so it cannot be dragged until the page mounts `mountTitleBar`. A skeleton whose default value breaks the "hello world" case is worse than no skeleton, so the default is `FLAG_RESIZABLE | FLAG_CENTER` — the side that works even when the page does nothing.
|
|
45
|
+
|
|
46
|
+
**It is not re-exported from the main entry.** `app.ts` imports `openShell` from `./index`; exporting it back from `index.ts` would make that a cycle. It is a subpath like `./state` and `./ipc`, and a static assertion in `tests/app.test.ts` keeps it that way.
|
|
47
|
+
|
|
48
|
+
**Static assets are inline strings only.** `import html from "./index.html" with { type: "text" }` yields a compile-time constant: nothing touches the filesystem, no temp files, and it survives `bun build --compile`. Reading from a directory has to cope with `import.meta.dir` pointing at `B:\~BUN\root` after packing — the `PACKED` branch in `src/host.ts` exists precisely for that. Five lines in a `routes` callback is a better trade than leaking that problem into the skeleton.
|
|
49
|
+
|
|
50
|
+
- `decideWs()` — the `/ws` decision, extracted as a pure function because it is the only branch in the skeleton and it has three ways to be wrong. `/ws` with IPC disabled is a **404, not a 403**: a 403 advertises that a channel exists. The token check must happen **before** the upgrade — after upgrading there is no chance to refuse. And the path comparison must come first, or a caller's catch-all route (the usual SPA fallback) swallows the upgrade request, leaving a page that can never connect while HTTP looks perfectly healthy.
|
|
51
|
+
|
|
52
|
+
### Changed
|
|
53
|
+
|
|
54
|
+
- `attachPlacementStore` is now event-driven. It used to call `getBounds()` every 700 ms; it now takes a snapshot the moment a `STATE` event arrives and batches the disk write until 300 ms after the window settles. A window at rest costs zero native calls and zero disk writes, and a drag no longer writes on every frame. The `intervalMs` option is replaced by `debounceMs` (write delay) and `pumpMs` (internal pump cadence) — a 0.x minor is allowed to break that, and the old name would now be a lie.
|
|
55
|
+
|
|
56
|
+
Snapshot and write are deliberately separate. Once the window is destroyed, `format_bounds()` returns an empty string because `!g_hwnd`; reading at debounce time would therefore drop the last change — exactly the one made by "drag, then immediately click X", which is how most people close a window.
|
|
57
|
+
|
|
58
|
+
- `createApp`'s `stop()` and `wait()` wait for `EVENT_CLOSED` instead of polling `isAlive()` every 50 ms. The 5-second ceiling stays: if the event channel ever fails, the host must not hang forever, and the timeout also resolves the close promise so `wait()` cannot hang on a dead channel. `isAlive()` keeps its symbol and its API — it is still a legitimate instant snapshot — but it is no longer the waiting criterion. `app.events` is exposed as the single consumer, so `attachPlacementStore(shell, file, { events: app.events })` reuses it instead of starting a second one.
|
|
59
|
+
|
|
60
|
+
- Exported symbols 33 → 34 and TypeScript declarations 32 → 33 (`shell_poll_events`). `native-surface.test.ts`'s hand-written `DECLARED` list and its total-count assertion moved with it.
|
|
61
|
+
|
|
62
|
+
- Twelve assertions in `tests/app.test.ts` plus seven in `scripts/consumer-probe.ts`. The probe now also exercises `bun-app-window/app` from the installed package.
|
|
63
|
+
- Three hardcoded counts in the docs are gone (`40 assertions`, `8 subpaths`, `182 test cases`): all three had already drifted. The consumer probe reports its own count now, and `AGENTS.md` says so.
|
|
64
|
+
|
|
65
|
+
### Note
|
|
66
|
+
|
|
67
|
+
- The pump timer's `unref` is not an implementation detail. An unref'd timer does not hold the event loop open, and a promise that never resolves does not either — so a host that `await`s for close while relying on an unref'd pump will exit with the window still open, which looks like "the app flashes and quits". `./state` unrefs (a persistence timer should not keep a process alive); `./app` pins the timer with `keepAlive: true` because waiting for close is exactly what it needs to survive.
|
|
68
|
+
|
|
69
|
+
- The acceptance for this release was bidirectional rather than one-sided. Commenting out `emit_event(SHELL_EVENT_STATE, ...)`, rebuilding, and running `scripts/probe-window-events.ts` turned it from 21/21 green to 11/21 red with `exit=1` — while the ten `CLOSED` assertions stayed green, confirming the injection was isolated to the state path. Restoring and rebuilding returned 21/21, and `verify-dll.ts` reported the same sha256 as before the injection, which also proves the build is reproducible. `docs/pitfalls.md` 2.4k records the four failure modes found along the way.
|
|
70
|
+
|
|
71
|
+
- This is **not** a competitor to `Bun.WebView`, and the READMEs now say so explicitly. `Bun.WebView` is a *headless* browser for automation, testing and scraping: it has no window, and it can only screenshot its own page. This package draws a real desktop window — system title bar, DPI awareness, snap layouts, taskbar identity — and its capture tool can photograph **any** window, which is the whole point for computer-use. Comparing API surface between the two is comparing different problems.
|
|
8
72
|
|
|
9
73
|
## [0.3.0] - 2026-10-21
|
|
10
74
|
|
|
@@ -221,7 +285,8 @@ First public release.
|
|
|
221
285
|
- Not yet verified in managed environments that enforce AppLocker, WDAC, or third-party EDR behaviour blocking.
|
|
222
286
|
- No commercially trusted code signing: only a self-signed certificate has been exercised end to end.
|
|
223
287
|
|
|
224
|
-
[Unreleased]: https://github.com/TiancongLx/bun-app-window/compare/v0.
|
|
288
|
+
[Unreleased]: https://github.com/TiancongLx/bun-app-window/compare/v0.4.0...HEAD
|
|
289
|
+
[0.4.0]: https://github.com/TiancongLx/bun-app-window/compare/v0.3.0...v0.4.0
|
|
225
290
|
[0.3.0]: https://github.com/TiancongLx/bun-app-window/compare/v0.2.2...v0.3.0
|
|
226
291
|
[0.2.2]: https://github.com/TiancongLx/bun-app-window/compare/v0.2.1...v0.2.2
|
|
227
292
|
[0.2.1]: https://github.com/TiancongLx/bun-app-window/compare/v0.2.0...v0.2.1
|
package/README.md
CHANGED
|
@@ -84,7 +84,8 @@ Note: `createShell(opts)` only resolves and loads the native DLL without opening
|
|
|
84
84
|
| `start()` | Opens the window and returns `{ rc, error }`. Returns immediately while the UI message loop runs on a dedicated native thread |
|
|
85
85
|
| `stop()` | Destroys the window and releases native resources. Safe to call multiple times (idempotent) |
|
|
86
86
|
| `show()` | Reveals the window. Typically paired with `FLAG_DEFER_SHOW` to display the window only after the web UI is ready |
|
|
87
|
-
| `isAlive()` | Checks whether the native window still exists (unlike `state()`, which cannot distinguish between unstarted and destroyed states) |
|
|
87
|
+
| `isAlive()` | Checks whether the native window still exists (unlike `state()`, which cannot distinguish between unstarted and destroyed states). To wait for close, use `pollEvents()` and watch for `CLOSED` instead of polling this |
|
|
88
|
+
| `pollEvents(max?)` | Drains window lifecycle events (`STATE` / `CLOSED`); synchronous and consuming. See "Window events" below |
|
|
88
89
|
| `state()` | Returns current window state: `{ maximized, minimized, focused, dark, raw }` |
|
|
89
90
|
| `lastError()` | Returns the most recent failure message, or an empty string if no error occurred |
|
|
90
91
|
| `setTitle` / `minimize` / `toggleMaximize` / `close` | Standard window control operations; each returns a status code `rc` |
|
|
@@ -138,6 +139,74 @@ the default picks the safe side.
|
|
|
138
139
|
|
|
139
140
|
Additional capabilities outside core window management are modularized into dedicated subpath exports.
|
|
140
141
|
|
|
142
|
+
### `bun-app-window/app` — Application Skeleton
|
|
143
|
+
|
|
144
|
+
The first app you write with this package is about 30 lines of business code and **180 lines of
|
|
145
|
+
host boilerplate**: picking a safe port, minting an auth token, handling the WebSocket upgrade,
|
|
146
|
+
serving the HTML as a string, wiring `createIpcServer` into Bun's websocket callbacks, composing
|
|
147
|
+
window flags, handling `openShell` failure, waiting for close (`EVENT_CLOSED`), and calling
|
|
148
|
+
`stop()` on two things in the right order. Each piece is easy; they are only correct in a specific
|
|
149
|
+
order (`stop()` must wait for the window to actually die before closing the server, or the next
|
|
150
|
+
start hits "already started"). `app` collapses all of it into one call:
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
// main.ts
|
|
154
|
+
import { createApp } from "bun-app-window/app";
|
|
155
|
+
import { BROWSER_UI_ALL, FLAG_FRAMELESS } from "bun-app-window";
|
|
156
|
+
import html from "./index.html" with { type: "text" };
|
|
157
|
+
|
|
158
|
+
const app = await createApp({
|
|
159
|
+
html,
|
|
160
|
+
title: "My App",
|
|
161
|
+
flags: FLAG_FRAMELESS, // opt in to a self-drawn title bar
|
|
162
|
+
browserUi: BROWSER_UI_ALL,
|
|
163
|
+
service: {
|
|
164
|
+
greet: (name: string) => `hi ${name}`,
|
|
165
|
+
pickFolder: () => app.shell.pickFolder(),
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
await app.wait(); // resolves when the window closes
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
```ts
|
|
173
|
+
// page.ts — inside index.html's bundle
|
|
174
|
+
import { connect } from "bun-app-window/ipc";
|
|
175
|
+
const api = await connect(); // token comes from the page URL's ?t=
|
|
176
|
+
await api.greet("Ada");
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
What it does for you:
|
|
180
|
+
|
|
181
|
+
- Binds a browser-safe port (never one that yields `ERR_UNSAFE_PORT`) and serves your HTML.
|
|
182
|
+
- Generates a per-run token and guards `/ws` with it **before** the upgrade — after upgrading there
|
|
183
|
+
is no chance to refuse. `/ws` with IPC disabled is a `404`, not a `403`: a 403 would advertise
|
|
184
|
+
that a channel exists.
|
|
185
|
+
- Wires `createIpcServer` into the websocket `open` / `message` / `close` callbacks so
|
|
186
|
+
`ipc.emit()` reaches the page.
|
|
187
|
+
- `stop()` is ordered and idempotent: `shell.stop()` → wait for `EVENT_CLOSED` (5 s ceiling) →
|
|
188
|
+
`server.stop()`.
|
|
189
|
+
- Starts exactly one window-event pump: `app.events`. It is also what waits for close, so it is
|
|
190
|
+
the single consumer. Pass `{ events: app.events }` when you need `attachPlacementStore`.
|
|
191
|
+
- Startup failures leave nothing half-started: if `openShell` throws, the bound port is released.
|
|
192
|
+
|
|
193
|
+
Notes:
|
|
194
|
+
|
|
195
|
+
- **Does not default to `FLAG_FRAMELESS`.** A frameless window has no system title bar, so it cannot
|
|
196
|
+
be dragged until the page mounts `mountTitleBar`. The default is the side that works even when the
|
|
197
|
+
page does nothing.
|
|
198
|
+
- **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`. For
|
|
200
|
+
anything read from disk, write a `routes` function — five lines, and the path-resolution problem
|
|
201
|
+
stays yours instead of leaking into the skeleton.
|
|
202
|
+
- **Not re-exported from the main entry.** It is a subpath like `./state` and `./ipc`. It imports
|
|
203
|
+
`openShell` from `./index`; exporting it from `index.ts` would make that a cycle.
|
|
204
|
+
|
|
205
|
+
This is **not** a replacement for `Bun.WebView`. That one is a *headless* browser for automation,
|
|
206
|
+
testing and scraping — it has no window, and it can only screenshot its own page. This package
|
|
207
|
+
draws a real desktop window with a system title bar, DPI awareness, snap layouts and taskbar
|
|
208
|
+
identity. The two do not compete.
|
|
209
|
+
|
|
141
210
|
### `bun-app-window/ipc` — Typed RPC & Event Bus
|
|
142
211
|
|
|
143
212
|
Share a single TypeScript contract between the host and the web page for compile-time verification of method signatures and event payloads:
|
|
@@ -233,7 +302,7 @@ if (saved) restorePlacement(shell, saved);
|
|
|
233
302
|
|
|
234
303
|
shell.show();
|
|
235
304
|
|
|
236
|
-
const store = attachPlacementStore(shell, FILE); //
|
|
305
|
+
const store = attachPlacementStore(shell, FILE); // Subscribes to window events; writes once the drag settles
|
|
237
306
|
process.on("beforeExit", () => store.flush());
|
|
238
307
|
```
|
|
239
308
|
|
|
@@ -243,7 +312,60 @@ The native placement layer handles edge cases automatically:
|
|
|
243
312
|
- Translates coordinates across monitors with different DPI scaling factors.
|
|
244
313
|
- Validates and clamps off-screen coordinates when monitor topology changes.
|
|
245
314
|
|
|
246
|
-
|
|
315
|
+
Writes are event-driven: a state change takes a snapshot **immediately** (a microsecond-scale
|
|
316
|
+
read), and the disk write is batched 300 ms after the window settles. A window at rest costs
|
|
317
|
+
neither a native call nor a disk write. Tune the write delay with `debounceMs`; tune the
|
|
318
|
+
internal event pump's cadence with `pumpMs`.
|
|
319
|
+
|
|
320
|
+
If you already have an event pump (for example from `createApp`), pass it in so two consumers
|
|
321
|
+
do not race for the same events:
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
import { createApp } from "bun-app-window/app";
|
|
325
|
+
import { attachPlacementStore } from "bun-app-window/state";
|
|
326
|
+
|
|
327
|
+
const app = await createApp({ html });
|
|
328
|
+
const store = attachPlacementStore(app.shell, FILE, { events: app.events });
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
For one-off reads and writes without any subscription, use `capturePlacement` and
|
|
332
|
+
`savePlacement` directly.
|
|
333
|
+
|
|
334
|
+
### Window events
|
|
335
|
+
|
|
336
|
+
The host does not have to poll to learn that the window changed — `pollEvents()` returns the
|
|
337
|
+
event stream directly:
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
import { EVENT_CLOSED, EVENT_STATE } from "bun-app-window/events";
|
|
341
|
+
|
|
342
|
+
for (const e of shell.pollEvents()) {
|
|
343
|
+
if (e.type === EVENT_CLOSED) break; // window is gone, the native singleton is released
|
|
344
|
+
if (e.type === EVENT_STATE && e.maximized) { /* ... */ }
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
`pollEvents()` is **consuming**: drained records never come back, so a process should have
|
|
349
|
+
exactly one consumer. For a fixed cadence plus multi-subscriber fan-out, use
|
|
350
|
+
`attachEventPump()` from `./events`:
|
|
351
|
+
|
|
352
|
+
```ts
|
|
353
|
+
import { EVENT_CLOSED, attachEventPump } from "bun-app-window/events";
|
|
354
|
+
|
|
355
|
+
const pump = attachEventPump(shell, { intervalMs: 16 });
|
|
356
|
+
pump.on((e) => { if (e.type === EVENT_CLOSED) pump.stop(); });
|
|
357
|
+
pump.onGap((missing) => console.warn(`missed ${missing} events`));
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
- A `STATE` payload is a **snapshot**: `width` / `height` / `dpi` plus four flags
|
|
361
|
+
(`maximized` / `minimized` / `focused` / `dark`).
|
|
362
|
+
- `CLOSED` is terminal. It is emitted by the native layer after teardown has fully completed
|
|
363
|
+
and the window handle is gone, so "seeing `CLOSED`" is equivalent to "safe to restart the
|
|
364
|
+
window" — that is what makes it stronger than `isAlive()`.
|
|
365
|
+
- The native ring buffer holds 128 records and drops the oldest when full. Losses are reported
|
|
366
|
+
faithfully as gaps in `seq` (via `onGap`), never disguised as "nothing happened".
|
|
367
|
+
- The pump timer is `unref`'d by default (it will not keep the process alive). Pass
|
|
368
|
+
`keepAlive: true` when you rely on it to wait for window close.
|
|
247
369
|
|
|
248
370
|
### `bun-app-window/debug` — Diagnostics
|
|
249
371
|
|
|
@@ -421,7 +543,7 @@ bun run scripts/gate.ts --clean # same gates, but inside a fresh clone of H
|
|
|
421
543
|
|
|
422
544
|
`-Clean` reproduces the conditions a hosted runner would provide: it clones `HEAD` into `%TEMP%`, installs from the lockfile, and runs the gates there, so local artefacts (`.build/`, `node_modules/`, `vendor/webview2/`) cannot mask a missing file. It takes about two seconds. Uncommitted changes are reported and excluded — run without `-Clean` to verify the working tree as-is.
|
|
423
545
|
|
|
424
|
-
The gate has three stages: `bun check`, `bun test`, and consumer acceptance (`scripts/verify-pack.ts`). The third packs the tarball, installs it into a scratch project under `%TEMP%`, type-checks that project, and runs `scripts/consumer-probe.ts` against the installed package —
|
|
546
|
+
The gate has three stages: `bun check`, `bun test`, and consumer acceptance (`scripts/verify-pack.ts`). The third packs the tarball, installs it into a scratch project under `%TEMP%`, type-checks that project, and runs `scripts/consumer-probe.ts` against the installed package — assertions covering every subpath, the DLL actually being `dlopen`-ed out of `node_modules`, and `libInfo.source === "package"` (the probe reports its own count). It runs offline in about 0.4 s and denies by default: a file packed without being listed in the allow-list fails the gate. Pass `-SkipPack` only when the gate itself is what you are changing.
|
|
425
547
|
|
|
426
548
|
The underlying commands remain available directly:
|
|
427
549
|
|
package/README.zh-CN.md
CHANGED
|
@@ -84,7 +84,8 @@ shell.setTitle("Doing work…");
|
|
|
84
84
|
| `start()` | 创建并启动窗口,返回 `{ rc, error }`。调用后立即返回,UI 消息循环运行于独立原生线程 |
|
|
85
85
|
| `stop()` | 销毁窗口并释放底层资源,支持幂等重复调用 |
|
|
86
86
|
| `show()` | 显示窗口。常配合 `FLAG_DEFER_SHOW` 使用,实现在页面渲染就绪后再展现窗口 |
|
|
87
|
-
| `isAlive()` | 检测窗口是否存活(注:`state()`
|
|
87
|
+
| `isAlive()` | 检测窗口是否存活(注:`state()` 无法区分「尚未启动」与「已销毁」状态)。等窗口关闭请用 `pollEvents()` 的 `CLOSED`,别轮询它 |
|
|
88
|
+
| `pollEvents(max?)` | 批量拉取窗口生命周期事件(`STATE` / `CLOSED`),同步、消费式。详见下文「窗口事件」 |
|
|
88
89
|
| `state()` | 获取当前窗口状态:`{ maximized, minimized, focused, dark, raw }` |
|
|
89
90
|
| `lastError()` | 获取最近一次操作失败的错误信息,无错误时返回空字符串 |
|
|
90
91
|
| `setTitle` / `minimize` / `toggleMaximize` / `close` | 基础窗口控制操作,返回状态码 `rc` |
|
|
@@ -136,6 +137,67 @@ shell.setTitle("Doing work…");
|
|
|
136
137
|
|
|
137
138
|
核心窗口控制之外的扩展能力均拆分在独立子路径下,可按需引入。
|
|
138
139
|
|
|
140
|
+
### `bun-app-window/app` — 应用骨架
|
|
141
|
+
|
|
142
|
+
用这个包写第一个应用时,业务代码可能只有 30 行,但**宿主样板是 180 行**:挑安全端口、
|
|
143
|
+
生成鉴权 token、处理 WebSocket 升级、把 HTML 当字符串吐出去、把 `createIpcServer`
|
|
144
|
+
接到 Bun 的 websocket 回调上、拼窗口 flags、处理 `openShell` 失败、等窗口关闭
|
|
145
|
+
(`EVENT_CLOSED`)、按顺序 `stop()` 两个东西。每一段单看都不难,但它们**只在特定顺序下正确**
|
|
146
|
+
(`stop()` 不先等窗口真的销毁就关服务,下次启动会撞上 "already started")。
|
|
147
|
+
`app` 把这些收成一次调用:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
// main.ts
|
|
151
|
+
import { createApp } from "bun-app-window/app";
|
|
152
|
+
import { BROWSER_UI_ALL, FLAG_FRAMELESS } from "bun-app-window";
|
|
153
|
+
import html from "./index.html" with { type: "text" };
|
|
154
|
+
|
|
155
|
+
const app = await createApp({
|
|
156
|
+
html,
|
|
157
|
+
title: "我的应用",
|
|
158
|
+
flags: FLAG_FRAMELESS, // 想要自绘标题栏就显式加
|
|
159
|
+
browserUi: BROWSER_UI_ALL,
|
|
160
|
+
service: {
|
|
161
|
+
greet: (name: string) => `hi ${name}`,
|
|
162
|
+
pickFolder: () => app.shell.pickFolder(),
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await app.wait(); // 窗口关闭时 resolve
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
// page.ts —— index.html 的 bundle 里
|
|
171
|
+
import { connect } from "bun-app-window/ipc";
|
|
172
|
+
const api = await connect(); // token 从页面 URL 的 ?t= 取
|
|
173
|
+
await api.greet("Ada");
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
它替你做的事:
|
|
177
|
+
|
|
178
|
+
- 绑定浏览器安全的端口(绝不会是 `ERR_UNSAFE_PORT` 那几个),并把 HTML 服务出去。
|
|
179
|
+
- 每次运行生成一个 token,并在**升级之前**用它校验 `/ws` —— 升级之后就没有机会拒绝了。
|
|
180
|
+
`ipc` 没开时 `/ws` 是 `404` 而不是 `403`:403 等于告诉对方「这里有通道」。
|
|
181
|
+
- 把 `createIpcServer` 接到 websocket 的 `open` / `message` / `close` 上,`ipc.emit()` 才能到页面。
|
|
182
|
+
- `stop()` 有序且幂等:`shell.stop()` → 等 `EVENT_CLOSED`(带 5 秒上限)→ `server.stop()`。
|
|
183
|
+
- 起一个窗口事件泵并且只起一个:`app.events`。它同时承担「等窗口关闭」,
|
|
184
|
+
所以是唯一的事件消费者;`attachPlacementStore` 要用它时传 `{ events: app.events }`。
|
|
185
|
+
- 启动失败不留半启动状态:`openShell` 抛错时,已绑定的端口会被释放。
|
|
186
|
+
|
|
187
|
+
注意事项:
|
|
188
|
+
|
|
189
|
+
- **默认不带 `FLAG_FRAMELESS`。** 无边框窗口没有系统标题栏就拖不动,必须由页面先挂上
|
|
190
|
+
`mountTitleBar`。默认值选的是「页面什么都不做也能用」的那一侧。
|
|
191
|
+
- **静态资源只吃内联字符串。** `with { type: "text" }` 得到的是编译期常量:不碰文件系统、
|
|
192
|
+
不产生临时文件、`bun build --compile` 后仍然成立。要从磁盘读东西就写个 `routes` 函数 ——
|
|
193
|
+
五行,而且路径解析的问题留在你这边,不会渗进骨架。
|
|
194
|
+
- **不从主入口导出。** 它和 `./state`、`./ipc` 一样是子路径。它从 `./index` 导入
|
|
195
|
+
`openShell`;若 `index.ts` 反过来导出它,就成了循环。
|
|
196
|
+
|
|
197
|
+
**这不是 `Bun.WebView` 的替代品。** 那个是 headless 浏览器,做自动化 / 测试 / 抓取 ——
|
|
198
|
+
没有窗口,而且只能截自己的页面。本项目画的是真实桌面窗口:系统标题栏、DPI 感知、
|
|
199
|
+
贴边分屏、任务栏归属。两者不在同一条赛道上。
|
|
200
|
+
|
|
139
201
|
### `bun-app-window/ipc` — 类型化双向通信与事件总线
|
|
140
202
|
|
|
141
203
|
宿主与前端页面共享同一份 TypeScript 类型声明,客户端通过泛型在编译期完成方法签名与事件负载校验:
|
|
@@ -231,7 +293,7 @@ if (saved) restorePlacement(shell, saved);
|
|
|
231
293
|
|
|
232
294
|
shell.show();
|
|
233
295
|
|
|
234
|
-
const store = attachPlacementStore(shell, FILE); //
|
|
296
|
+
const store = attachPlacementStore(shell, FILE); // 订阅窗口事件,拖动停手后写一次盘
|
|
235
297
|
process.on("beforeExit", () => store.flush());
|
|
236
298
|
```
|
|
237
299
|
|
|
@@ -241,7 +303,52 @@ process.on("beforeExit", () => store.flush());
|
|
|
241
303
|
- 跨显示器不同缩放比下的 DPI 坐标换算。
|
|
242
304
|
- 屏幕拓扑变化(如拔除副屏)后的屏外坐标越界校验与修正。
|
|
243
305
|
|
|
244
|
-
|
|
306
|
+
写入是事件驱动的:收到状态变化**立刻**取快照(微秒级),写盘攒到停手 300 ms 后做一次。
|
|
307
|
+
窗口静止时既没有原生调用也没有磁盘写入。用 `debounceMs` 调整写盘延迟;用 `pumpMs`
|
|
308
|
+
调整内部事件泵的拉取节奏。
|
|
309
|
+
|
|
310
|
+
若已有事件泵(例如用了 `createApp`),把它传进来,避免两个消费者争抢同一批事件:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
import { createApp } from "bun-app-window/app";
|
|
314
|
+
import { attachPlacementStore } from "bun-app-window/state";
|
|
315
|
+
|
|
316
|
+
const app = await createApp({ html });
|
|
317
|
+
const store = attachPlacementStore(app.shell, FILE, { events: app.events });
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
若无需后台订阅,也可直接调用 `capturePlacement` 与 `savePlacement` 执行单次手动读写。
|
|
321
|
+
|
|
322
|
+
### 窗口事件
|
|
323
|
+
|
|
324
|
+
宿主要知道「窗口状态变了」,不必轮询——`pollEvents()` 直接给出事件流:
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
import { EVENT_CLOSED, EVENT_STATE } from "bun-app-window/events";
|
|
328
|
+
|
|
329
|
+
for (const e of shell.pollEvents()) {
|
|
330
|
+
if (e.type === EVENT_CLOSED) break; // 窗口已销毁,原生单例已释放
|
|
331
|
+
if (e.type === EVENT_STATE && e.maximized) { /* ... */ }
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
`pollEvents()` 是**消费式**的:读走的记录不会再来。所以一个进程只应有一个消费者。
|
|
336
|
+
想要固定节奏与多路订阅时,用 `./events` 的 `attachEventPump()`:
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
import { EVENT_CLOSED, attachEventPump } from "bun-app-window/events";
|
|
340
|
+
|
|
341
|
+
const pump = attachEventPump(shell, { intervalMs: 16 });
|
|
342
|
+
pump.on((e) => { if (e.type === EVENT_CLOSED) pump.stop(); });
|
|
343
|
+
pump.onGap((missing) => console.warn(`漏了 ${missing} 条事件`));
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
- `STATE` 载荷是**快照**:`width` / `height` / `dpi` 与四位状态(`maximized` / `minimized` / `focused` / `dark`)。
|
|
347
|
+
- `CLOSED` 是终态。它由原生侧在 teardown 全部做完、窗口句柄已置空之后写出,所以
|
|
348
|
+
「看到 `CLOSED`」等价于「可以安全重启窗口」——这是它优于 `isAlive()` 的地方。
|
|
349
|
+
- 原生侧环形队列容量 128 条,满时丢最旧。丢失量通过 `seq` 缺口如实上报(`onGap`),
|
|
350
|
+
不会伪装成「什么都没发生」。
|
|
351
|
+
- 事件泵的定时器默认 `unref`(不阻止进程退出)。若靠它等窗口关闭,传 `keepAlive: true`。
|
|
245
352
|
|
|
246
353
|
### `bun-app-window/debug` — 诊断与排障接口
|
|
247
354
|
|
|
@@ -417,7 +524,7 @@ bun run scripts/gate.ts --clean # 同上,但在 HEAD 的干净副本里
|
|
|
417
524
|
|
|
418
525
|
`-Clean` 复现了托管 runner 的条件:把 `HEAD` clone 到 `%TEMP%`,按 lockfile 安装依赖,再在其中跑门禁。这样本地产物(`.build/`、`node_modules/`、`vendor/webview2/`)就无法掩盖缺失的文件。耗时约两秒。未提交改动会被提示并排除在干净副本之外——如需验证工作区现状,请去掉 `-Clean`。
|
|
419
526
|
|
|
420
|
-
门禁分三段:`bun check`、`bun test`,以及消费方验收(`scripts/verify-pack.ts`)。第三段会把 tarball 打进 `%TEMP%`,装进一个临时工程,对那个工程做类型检查,再运行 `scripts/consumer-probe.ts
|
|
527
|
+
门禁分三段:`bun check`、`bun test`,以及消费方验收(`scripts/verify-pack.ts`)。第三段会把 tarball 打进 `%TEMP%`,装进一个临时工程,对那个工程做类型检查,再运行 `scripts/consumer-probe.ts`——断言覆盖全部子路径、DLL 是否真从 `node_modules` 里被 `dlopen`、以及 `libInfo.source` 是否为 `package`(条数由探针自报)。离线可跑,约 0.4 秒,且默认拒绝:任何没在允许清单里登记的文件被打进包,门禁就红。只有在改动门禁自身时才用 `-SkipPack`。
|
|
421
528
|
|
|
422
529
|
底层命令仍可直接调用:
|
|
423
530
|
|
package/README.zh-TW.md
CHANGED
|
@@ -84,7 +84,8 @@ shell.setTitle("Doing work…");
|
|
|
84
84
|
| `start()` | 建立並啟動視窗,回傳 `{ rc, error }`。呼叫後立即回傳,UI 訊息迴圈執行於獨立原生執行緒 |
|
|
85
85
|
| `stop()` | 銷毀視窗並釋放底層資源,支援冪等重複呼叫 |
|
|
86
86
|
| `show()` | 顯示視窗。常搭配 `FLAG_DEFER_SHOW` 使用,實現在頁面渲染就緒後再展現視窗 |
|
|
87
|
-
| `isAlive()` | 檢測視窗是否存活(註:`state()`
|
|
87
|
+
| `isAlive()` | 檢測視窗是否存活(註:`state()` 無法區分「尚未啟動」與「已銷毀」狀態)。等視窗關閉請用 `pollEvents()` 的 `CLOSED`,別輪詢它 |
|
|
88
|
+
| `pollEvents(max?)` | 批次拉取視窗生命週期事件(`STATE` / `CLOSED`),同步、消費式。詳見下文「視窗事件」 |
|
|
88
89
|
| `state()` | 取得目前視窗狀態:`{ maximized, minimized, focused, dark, raw }` |
|
|
89
90
|
| `lastError()` | 取得最近一次操作失敗的錯誤訊息,無錯誤時回傳空字串 |
|
|
90
91
|
| `setTitle` / `minimize` / `toggleMaximize` / `close` | 基礎視窗控制操作,回傳狀態碼 `rc` |
|
|
@@ -136,6 +137,67 @@ shell.setTitle("Doing work…");
|
|
|
136
137
|
|
|
137
138
|
核心視窗控制之外的擴充能力均拆分於獨立子路徑下,可依需求匯入。
|
|
138
139
|
|
|
140
|
+
### `bun-app-window/app` — 應用程式骨架
|
|
141
|
+
|
|
142
|
+
用這個套件寫第一個應用程式時,業務程式碼可能只有 30 行,但**宿主樣板是 180 行**:挑安全埠、
|
|
143
|
+
產生驗證 token、處理 WebSocket 升級、把 HTML 當字串吐出去、把 `createIpcServer`
|
|
144
|
+
接到 Bun 的 websocket 回呼上、拼視窗旗標、處理 `openShell` 失敗、等視窗關閉
|
|
145
|
+
(`EVENT_CLOSED`)、依序 `stop()` 兩個東西。每一段單看都不難,但它們**只在特定順序下正確**
|
|
146
|
+
(`stop()` 不先等視窗真的銷毀就關服務,下次啟動會撞上 "already started")。
|
|
147
|
+
`app` 把這些收成一次呼叫:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
// main.ts
|
|
151
|
+
import { createApp } from "bun-app-window/app";
|
|
152
|
+
import { BROWSER_UI_ALL, FLAG_FRAMELESS } from "bun-app-window";
|
|
153
|
+
import html from "./index.html" with { type: "text" };
|
|
154
|
+
|
|
155
|
+
const app = await createApp({
|
|
156
|
+
html,
|
|
157
|
+
title: "我的應用程式",
|
|
158
|
+
flags: FLAG_FRAMELESS, // 想要自繪標題列就明確加上
|
|
159
|
+
browserUi: BROWSER_UI_ALL,
|
|
160
|
+
service: {
|
|
161
|
+
greet: (name: string) => `hi ${name}`,
|
|
162
|
+
pickFolder: () => app.shell.pickFolder(),
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
await app.wait(); // 視窗關閉時 resolve
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
// page.ts —— index.html 的 bundle 內
|
|
171
|
+
import { connect } from "bun-app-window/ipc";
|
|
172
|
+
const api = await connect(); // token 從頁面 URL 的 ?t= 取得
|
|
173
|
+
await api.greet("Ada");
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
它替你做的事:
|
|
177
|
+
|
|
178
|
+
- 綁定瀏覽器安全的埠(絕不會是 `ERR_UNSAFE_PORT` 那幾個),並把 HTML 服務出去。
|
|
179
|
+
- 每次執行產生一個 token,並在**升級之前**用它校驗 `/ws` —— 升級之後就沒有機會拒絕了。
|
|
180
|
+
`ipc` 未開啟時 `/ws` 是 `404` 而不是 `403`:403 等於告訴對方「這裡有通道」。
|
|
181
|
+
- 把 `createIpcServer` 接到 websocket 的 `open` / `message` / `close` 上,`ipc.emit()` 才能到頁面。
|
|
182
|
+
- `stop()` 有序且冪等:`shell.stop()` → 等 `EVENT_CLOSED`(帶 5 秒上限)→ `server.stop()`。
|
|
183
|
+
- 起一個視窗事件泵並且只起一個:`app.events`。它同時承擔「等視窗關閉」,
|
|
184
|
+
所以是唯一的事件消費者;`attachPlacementStore` 要用它時傳 `{ events: app.events }`。
|
|
185
|
+
- 啟動失敗不留半啟動狀態:`openShell` 拋錯時,已綁定的埠會被釋放。
|
|
186
|
+
|
|
187
|
+
注意事項:
|
|
188
|
+
|
|
189
|
+
- **預設不帶 `FLAG_FRAMELESS`。** 無邊框視窗沒有系統標題列就拖不動,必須由頁面先掛上
|
|
190
|
+
`mountTitleBar`。預設值選的是「頁面什麼都不做也能用」的那一側。
|
|
191
|
+
- **靜態資源只吃內嵌字串。** `with { type: "text" }` 得到的是編譯期常數:不碰檔案系統、
|
|
192
|
+
不產生暫存檔、`bun build --compile` 後仍然成立。要從磁碟讀東西就寫個 `routes` 函式 ——
|
|
193
|
+
五行,而且路徑解析的問題留在你這邊,不會滲進骨架。
|
|
194
|
+
- **不從主入口匯出。** 它和 `./state`、`./ipc` 一樣是子路徑。它從 `./index` 匯入
|
|
195
|
+
`openShell`;若 `index.ts` 反過來匯出它,就成了循環。
|
|
196
|
+
|
|
197
|
+
**這不是 `Bun.WebView` 的替代品。** 那個是 headless 瀏覽器,做自動化 / 測試 / 擷取 ——
|
|
198
|
+
沒有視窗,而且只能截自己的頁面。本專案畫的是真實桌面視窗:系統標題列、DPI 感知、
|
|
199
|
+
貼齊版面配置、工作列歸屬。兩者不在同一條賽道上。
|
|
200
|
+
|
|
139
201
|
### `bun-app-window/ipc` — 型別化雙向通訊與事件匯流排
|
|
140
202
|
|
|
141
203
|
宿主與前端頁面共用同一份 TypeScript 型別宣告,用戶端透過泛型在編譯期完成方法簽章與事件負載驗證:
|
|
@@ -231,7 +293,7 @@ if (saved) restorePlacement(shell, saved);
|
|
|
231
293
|
|
|
232
294
|
shell.show();
|
|
233
295
|
|
|
234
|
-
const store = attachPlacementStore(shell, FILE); //
|
|
296
|
+
const store = attachPlacementStore(shell, FILE); // 訂閱視窗事件,拖曳停下後寫入一次
|
|
235
297
|
process.on("beforeExit", () => store.flush());
|
|
236
298
|
```
|
|
237
299
|
|
|
@@ -241,7 +303,53 @@ process.on("beforeExit", () => store.flush());
|
|
|
241
303
|
- 跨顯示器不同縮放比例下的 DPI 座標換算。
|
|
242
304
|
- 螢幕拓撲變化(如拔除外接螢幕)後的螢幕外座標越界驗證與修正。
|
|
243
305
|
|
|
244
|
-
|
|
306
|
+
寫入是事件驅動的:收到狀態變化**立刻**取得快照(微秒級),寫入磁碟則攢到停下 300 ms 後
|
|
307
|
+
做一次。視窗靜止時既沒有原生呼叫也沒有磁碟寫入。用 `debounceMs` 調整寫入延遲;用
|
|
308
|
+
`pumpMs` 調整內部事件泵的拉取節奏。
|
|
309
|
+
|
|
310
|
+
若已有事件泵(例如用了 `createApp`),把它傳進來,避免兩個消費者爭搶同一批事件:
|
|
311
|
+
|
|
312
|
+
```ts
|
|
313
|
+
import { createApp } from "bun-app-window/app";
|
|
314
|
+
import { attachPlacementStore } from "bun-app-window/state";
|
|
315
|
+
|
|
316
|
+
const app = await createApp({ html });
|
|
317
|
+
const store = attachPlacementStore(app.shell, FILE, { events: app.events });
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
若無需背景訂閱,亦可直接呼叫 `capturePlacement` 與 `savePlacement` 執行單次手動讀寫。
|
|
321
|
+
|
|
322
|
+
### 視窗事件
|
|
323
|
+
|
|
324
|
+
宿主想知道「視窗狀態變了」,不必輪詢——`pollEvents()` 直接給出事件串流:
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
import { EVENT_CLOSED, EVENT_STATE } from "bun-app-window/events";
|
|
328
|
+
|
|
329
|
+
for (const e of shell.pollEvents()) {
|
|
330
|
+
if (e.type === EVENT_CLOSED) break; // 視窗已銷毀,原生單例已釋放
|
|
331
|
+
if (e.type === EVENT_STATE && e.maximized) { /* ... */ }
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
`pollEvents()` 是**消費式**的:讀走的記錄不會再來,所以一個行程只應有一個消費者。
|
|
336
|
+
想要固定節奏與多路訂閱時,用 `./events` 的 `attachEventPump()`:
|
|
337
|
+
|
|
338
|
+
```ts
|
|
339
|
+
import { EVENT_CLOSED, attachEventPump } from "bun-app-window/events";
|
|
340
|
+
|
|
341
|
+
const pump = attachEventPump(shell, { intervalMs: 16 });
|
|
342
|
+
pump.on((e) => { if (e.type === EVENT_CLOSED) pump.stop(); });
|
|
343
|
+
pump.onGap((missing) => console.warn(`漏了 ${missing} 條事件`));
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
- `STATE` 酬載是**快照**:`width` / `height` / `dpi` 與四位狀態
|
|
347
|
+
(`maximized` / `minimized` / `focused` / `dark`)。
|
|
348
|
+
- `CLOSED` 是終態。它由原生側在 teardown 全部做完、視窗代碼已置空之後寫出,所以
|
|
349
|
+
「看到 `CLOSED`」等價於「可以安全重啟視窗」——這是它優於 `isAlive()` 的地方。
|
|
350
|
+
- 原生側環形佇列容量 128 條,滿時丟最舊。遺失量透過 `seq` 缺口如實上報(`onGap`),
|
|
351
|
+
不會偽裝成「什麼都沒發生」。
|
|
352
|
+
- 事件泵的計時器預設 `unref`(不阻止行程退出)。若靠它等視窗關閉,傳 `keepAlive: true`。
|
|
245
353
|
|
|
246
354
|
### `bun-app-window/debug` — 診斷與除錯介面
|
|
247
355
|
|
|
@@ -417,7 +525,7 @@ bun run scripts/gate.ts --clean # 同上,但在 HEAD 的乾淨副本內
|
|
|
417
525
|
|
|
418
526
|
`-Clean` 重現了託管 runner 的條件:將 `HEAD` clone 到 `%TEMP%`,依 lockfile 安裝相依套件,再於其中執行把關。如此一來,本機產物(`.build/`、`node_modules/`、`vendor/webview2/`)便無法掩蓋缺少的檔案。耗時約兩秒。未提交的變更會被提示並排除於乾淨副本之外——如需驗證工作區現況,請移除 `-Clean`。
|
|
419
527
|
|
|
420
|
-
品質把關分三段:`bun check`、`bun test`,以及消費方驗收(`scripts/verify-pack.ts`)。第三段會把 tarball 打包到 `%TEMP%`,安裝進一個臨時專案,對該專案執行型別檢查,再執行 `scripts/consumer-probe.ts
|
|
528
|
+
品質把關分三段:`bun check`、`bun test`,以及消費方驗收(`scripts/verify-pack.ts`)。第三段會把 tarball 打包到 `%TEMP%`,安裝進一個臨時專案,對該專案執行型別檢查,再執行 `scripts/consumer-probe.ts`——斷言涵蓋全部子路徑、DLL 是否真的從 `node_modules` 被 `dlopen`、以及 `libInfo.source` 是否為 `package`(條數由探針自報)。可離線執行,約 0.4 秒,且預設拒絕:任何未在允許清單登記的檔案被打進套件,把關就會失敗。只有在改動把關腳本本身時才用 `-SkipPack`。
|
|
421
529
|
|
|
422
530
|
底層指令仍可直接呼叫:
|
|
423
531
|
|
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.4.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",
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
],
|
|
30
30
|
"exports": {
|
|
31
31
|
".": "./src/shell/index.ts",
|
|
32
|
+
"./app": "./src/shell/app.ts",
|
|
33
|
+
"./events": "./src/shell/events.ts",
|
|
32
34
|
"./embedded": "./src/shell/embedded.ts",
|
|
33
35
|
"./debug": "./src/shell/debug.ts",
|
|
34
36
|
"./state": "./src/shell/state.ts",
|