bun-app-window 0.0.0-stage → 0.2.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 +122 -0
- package/LICENSE +21 -0
- package/README.md +432 -2
- package/README.zh-CN.md +431 -0
- package/README.zh-TW.md +431 -0
- package/dist/libshell.dll +0 -0
- package/package.json +65 -5
- package/src/shell/assets.d.ts +16 -0
- package/src/shell/debug.ts +40 -0
- package/src/shell/embedded.ts +42 -0
- package/src/shell/index.ts +291 -0
- package/src/shell/ipc.ts +569 -0
- package/src/shell/native-asset.d.ts +18 -0
- package/src/shell/native.ts +92 -0
- package/src/shell/port.ts +77 -0
- package/src/shell/resolve.ts +211 -0
- package/src/shell/state.ts +211 -0
- package/src/shell/types.ts +144 -0
- package/src/ui/titlebar.ts +436 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.2.0] - 2026-10-20
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- The gate entry point moved from `scripts/gate.ps1` to `scripts/gate.ts`, run as `bun run scripts/gate.ts` (`--clean`, `--skip-pack`, `--keep-clone`). PowerShell-style `-Clean` / `-SkipPack` are accepted too. The reason is structural rather than aesthetic: `$LASTEXITCODE` is a global that only reflects the last native command, and every PowerShell function leaks its child-process output into the return-value pipeline — the exact mechanism behind the "reported GATE PASSED while the type check had failed" bug recorded in `docs/pitfalls.md` 5.3. `Bun.spawn`'s `exited` is bound per call, and `gate.ts` is itself covered by `bun check` (which caught a dead import on the first run). See ADR 0012.
|
|
14
|
+
- A shebang was verified rather than assumed: `#!/usr/bin/env bun` does nothing in PowerShell (Windows does not parse shebangs — `./gate.ts` is opened as a document), but it **does** work under the `sh` that Git ships, which is the environment `pre-push` runs in. The hook therefore calls `bun run scripts/gate.ts` explicitly, which is correct on every platform.
|
|
15
|
+
- The published tarball now carries only the shipping surface: 19 files (1.1 MB unpacked) instead of 101 (2.40 MB). The native C++ layer (`native/`) and the vendored upstream headers (`vendor/`) are no longer packed — they are only needed to rebuild `libshell.dll`, which requires cloning the repository. `bun run build:native` and `build:exe` are therefore repository-only entry points and do nothing useful inside `node_modules`.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- `bun-app-window/native` had no type declaration, so the documented `with { type: "file" }` import failed `bun check` with `TS2307` in any consumer. The subpath now uses a conditional export (`types` → `src/shell/native-asset.d.ts`, `default` → the DLL), leaving the runtime resolution unchanged: `import.meta.resolve()` still yields the real DLL path.
|
|
20
|
+
- Type-checking a project that depends on this package requires `@types/bun`; the requirement is now stated in the requirements table of all three READMEs, together with the exact error you get without it.
|
|
21
|
+
- `native/fetch.ps1` never worked on a fresh clone. Its first step gated on whether `vendor/webview/.git` existed, but the vendored copy is committed as plain files with no `.git` — so the condition was always true, `git clone` was attempted into a non-empty directory, `git` returned 128, and the script threw before ever reaching the SDK extraction step. The criterion is now the same three files `native/build.ps1` validates (`webview.h`, `EventToken.h`, `WebView2.h`), so the two scripts can no longer disagree about what "ready" means. **The script is now `native/fetch.ts`** (see below).
|
|
22
|
+
- Building the native layer on a fresh clone is now fully offline by default: `vendor/webview` ships in the repository and `vendor/webview2` is extracted from the also-committed `Microsoft.Web.WebView2.1.0.1150.38.nupkg`. Network access became opt-in via `-Webview` (re-clone upstream, overwriting 145 tracked files) and `-Sdk` (re-download and re-extract); `-Force` keeps the old "do everything" meaning. `-Webview` also drops the staged `.git` so the result stays identical in shape to the committed copy, instead of nesting a second repository inside this one.
|
|
23
|
+
- `fetch.ps1` now reports the three inputs with sizes plus the upstream `WEBVIEW_VERSION_*` values, and exits 2 when a required tool (`git.exe` / `aria2c.exe` / `7z.exe`) is missing rather than failing halfway through.
|
|
24
|
+
- `native/fetch.ps1` was migrated to `native/fetch.ts`, run as `bun run native/fetch.ts` (`--root`, `--webview`, `--sdk`, `--force`; PowerShell-style `-Webview` etc. are accepted too). Behaviour is unchanged, but the decision logic — four directory states for `vendor/webview`, four for the SDK — is now two pure functions (`decideWebview`, `decideSdk`) covered by 14 cases in `tests/fetch.test.ts`. The state that had never been verified before, "directory exists but inputs are incomplete", is now pinned: it must refuse with exit 1 and leave the tree untouched. A static assertion also reads `native/build.ps1` and requires every entry of `INPUTS` to appear in its pre-flight checks, so the two scripts cannot drift apart again. That assertion failed on its first run for a real reason — `build.ps1` builds those paths by joining `$wv` with a relative tail, so the full string never appears contiguously.
|
|
25
|
+
- `scripts/verify-pack.ts` leaked its temp workspace on every run. Two independent causes: `process.exit()` was called inside the `try` block, which terminates the process immediately and skips `finally`; and the success path's cleanup failure was swallowed by `.catch(() => {})`. All paths now funnel through `finally`, the exit code is set via `process.exitCode` instead of an immediate exit, and a failed removal prints a warning rather than disappearing. Verified on both paths — a normal run and an injected failure (a disallowed file added to `files`) each leave `%TEMP%` with zero `baw-pack-*` directories.
|
|
26
|
+
- The gate's third stage failed inside the `pre-push` hook with `tar (child): Cannot connect to C: resolve failed`, while passing in pwsh. `Bun.which("tar")` resolves to different binaries in the two environments: Git ships **GNU tar 1.35**, which reads `C:/...` as a `host:path` remote; pwsh resolves to `%SystemRoot%\System32\tar.exe` (bsdtar), which understands drive letters. Fixed with two independent layers — prefer System32's bsdtar on win32, and pass a bare filename with `cwd` instead of an absolute path, so a relative name never contains a colon.
|
|
27
|
+
- Entry-point detection could silently disable a script. `import.meta.main` becomes `false` under `bun run <path>` when the caller's cwd differs from the real directory only in letter case (`_repositories` vs `_Repositories`), which is exactly what Git's `sh` produces. The guard then skipped the whole entry block and the script exited `0` having done nothing — a gate that runs nothing and reports success. Detection is now `import.meta.main || argv[1]` matching `import.meta.path` after normalisation (`scripts/lib/entrypoint.ts`).
|
|
28
|
+
- The first version of that helper was itself the bug: it read `import.meta.main` / `import.meta.path` inside the imported module, where `main` is always `false` and `path` points at the helper. It returned `false` unconditionally and turned off all three entry points — silently. It is now a pure function taking both values from the caller, pinned by `tests/entrypoint.test.ts` (11 cases, including a regression test that the guard can never be constantly false).
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- `tests/gate.test.ts` and `tests/verify-pack.test.ts` close the gap that mattered most: the gate had no tests of its own. `gate.ts` is where every other guarantee enters, so a quiet regression there — a stage that always reports success, a short-circuit that stops short-circuiting — disables everything downstream while still printing `GATE PASSED`. The decision logic is now split into exported pure functions (`normalizeArg`, `parseGateArgs`, `checkedFileCount`, `parseTestCounts`, `parsePackSummary`, `parsePs1Summary`, `planStages`, `formatStage`, `summarize`) with execution injected through `GateDeps`, so the short-circuit itself is asserted by handing `runGate` a fake runner rather than by spawning four child processes. `checkPayload` — the deny-by-default allow-list that decides what may be published — is covered the same way. 49 new cases; both entry points are behind an entry-point guard, so importing them in tests has no side effects.
|
|
33
|
+
- `tests/entrypoint.test.ts` (11 cases) pins the entry-point guard, including a regression test that it can never be constantly false — the first implementation was, and it silently disabled all three scripts.
|
|
34
|
+
- The gate gained a second stage: syntax-checks every remaining `.ps1` with `[System.Management.Automation.Language.Parser]::ParseFile`. The 18 PowerShell scripts that stay have neither type checking nor tests, so this is the cheapest available guard — it catches unbalanced braces and misspelled keywords, not semantic errors. It is explicitly a migration-period fence: when those scripts are migrated or formally kept, the stage should be deleted rather than left as permanent weight. Missing `pwsh` counts as a skip, not a failure.
|
|
35
|
+
- `scripts/verify-pack.ts` and `scripts/consumer-probe.ts`: consumer acceptance as an executable gate. It packs the tarball, asserts every file against a deny-by-default allow-list, installs it into a scratch project under `%TEMP%`, type-checks that project, and runs 40 runtime assertions against the installed package — all eight subpaths resolving, the DLL actually being `dlopen`-ed out of `node_modules`, `libInfo.source === "package"`, IPC request/reply and event push, and a real `serveOnSafePort` bind. Offline, about 0.4 s.
|
|
36
|
+
- The gate gains a third stage that runs the above, on by default. `--skip-pack` exists for bootstrapping changes to the gate itself.
|
|
37
|
+
- `docs/pitfalls.md` 5.6 records that PowerShell's `Set-Content` rewrites line endings to CRLF regardless of the original, so a script that edits and then "restores" a file leaves a byte-level diff with an empty content diff. Encountered while smoke-testing the new `.ps1` syntax stage.
|
|
38
|
+
- `bun-app-window/native` is documented in all three READMEs (`import.meta.resolve` gives the DLL path; use `bun-app-window/embedded` for `bun build --compile` instead).
|
|
39
|
+
- ADR 0010 records the publish-surface decision, the conditional-export fix, and why `sideEffects: false` must not be added (it would let a bundler shake away the DLL embedding import in `embedded.ts`).
|
|
40
|
+
- ADR 0011 records the `fetch` criteria change: why the criterion must equal the files `build.ps1` actually validates, and why the network-touching steps became opt-in switches. The operational version lives in `docs/pitfalls.md` 5.5.
|
|
41
|
+
- ADR 0012 records the move of the gate to TypeScript: the three PowerShell mechanisms it removes (`$LASTEXITCODE` as a global, function output leaking into the return-value pipeline, no static checker), the measured behaviour of shebangs on Windows, and the migration boundary — the 6 scripts that are pure process orchestration migrate, the 14 that need Win32 APIs (`Add-Type` P/Invoke, `System.Drawing`, the certificate store, CIM) stay where they are.
|
|
42
|
+
|
|
43
|
+
## [0.1.0] - 2026-10-20
|
|
44
|
+
|
|
45
|
+
First public release.
|
|
46
|
+
|
|
47
|
+
### Added
|
|
48
|
+
|
|
49
|
+
#### Window shell
|
|
50
|
+
|
|
51
|
+
- `createShell()` builds a shell from `ShellOptions` without starting it; `openShell()` starts it and throws on failure. `Shell.start()` returns `{ rc, error }` instead of throwing, and the native layer creates the UI thread itself, so the caller's main thread is never blocked.
|
|
52
|
+
- Window flags, composable with bitwise OR: `FLAG_FRAMELESS` (1), `FLAG_TOPMOST` (2), `FLAG_CENTER` (4), `FLAG_RESIZABLE` (8), `FLAG_DEFER_SHOW` (16), `FLAG_ACCEPT_DROP` (32), `FLAG_START_MAXIMIZED` (64). `FLAG_CENTER` and `FLAG_START_MAXIMIZED` work in both decorated and frameless modes.
|
|
53
|
+
- Window controls: `setTitle`, `minimize`, `toggleMaximize`, `close`, `drag`, `beginResize(edge)` with `RESIZE_EDGE` matching the Win32 `HT*` constants, `setMinSize` (logical pixels, converted per current DPI), `show`, `setAppId`.
|
|
54
|
+
- `state()` reports `maximized` / `minimized` / `focused` / `dark`; `isAlive()` distinguishes "destroyed" from "never started"; `lastError()` copies the shared error slot instead of returning its pointer, so concurrent writers cannot corrupt the read.
|
|
55
|
+
- `stop()` is idempotent. Termination is dispatched to the UI thread, because `PostQuitMessage` only affects the calling thread's queue.
|
|
56
|
+
|
|
57
|
+
#### Desktop behaviour
|
|
58
|
+
|
|
59
|
+
- First frame without a white flash, via three cooperating mechanisms: subclassing installed before sizing, the initial size call kept (it sets the internal `m_is_size_set` flag), and the WebView2 default background replaced before navigation. Omitting any one of them reintroduces the flash.
|
|
60
|
+
- Frameless windows keep DPI-correct hit testing, edge resize, and snap layout support.
|
|
61
|
+
- Window state push with change detection plus a 16 ms trailing throttle, so drag-resizing does not flood the renderer over the cross-process bridge. Both window procedures keep their `WM_TIMER` branch.
|
|
62
|
+
- Zoom from `Ctrl` `+` / `-` / `0` and `Ctrl`+wheel, clamped to 0.25–3.0, with `setZoom` / `getZoom` for programmatic control. Scale factors cross the native boundary as milli-units because `wsprintfA` cannot format floats.
|
|
63
|
+
- File drop, reload, DevTools, and fullscreen toggles, all routed through one shared input pre-handler instead of being duplicated per window procedure.
|
|
64
|
+
|
|
65
|
+
#### Application chrome
|
|
66
|
+
|
|
67
|
+
- `setBrowserUi(mask)` and `ShellOptions.browserUi`, with `BROWSER_UI_MENU` (1), `BROWSER_UI_KEYS` (2), `BROWSER_UI_STATUSBAR` (4), `BROWSER_UI_SWIPE` (8), `BROWSER_UI_AUTOFILL` (16), and `BROWSER_UI_ALL`. The swipe switch lives on `ICoreWebView2Settings6`, not `Settings5`.
|
|
68
|
+
- `showContextMenu(x, y, items)` renders a native Win32 menu from a line-based `id|label` protocol, where a lone `-` is a separator. The selection comes back as `{ t: "menu", id }`.
|
|
69
|
+
- `pickFolder()` calls `SHBrowseForFolder` directly — no child process, no temporary script, no execution-policy dependency — and binds the dialog to the host window so it cannot fall behind it. It is synchronous and blocks the calling thread.
|
|
70
|
+
|
|
71
|
+
#### Typed IPC (`bun-app-window/ipc`)
|
|
72
|
+
|
|
73
|
+
- `createIpcServer({ service })` derives a route table from a plain object; `connect()` and `webSocketChannel()` hand the client a generic proxy typed against the service.
|
|
74
|
+
- Request/reply over WebSocket with per-call ids and concurrency-safe matching. Unknown methods and methods inherited from the prototype chain are rejected as errors returned to the caller, never thrown into the host.
|
|
75
|
+
- `undefined` returns normalize to `null`, because JSON drops the field. Error stacks are withheld unless `exposeStack` is enabled.
|
|
76
|
+
- Events broadcast to every attached peer; writes to dead connections are skipped without disturbing live ones. A throwing handler does not prevent the other handlers from running.
|
|
77
|
+
- Timeouts reject the pending call and late replies are discarded. Closing the channel rejects everything in flight, and calling after close fails immediately without writing.
|
|
78
|
+
|
|
79
|
+
#### Windows 11 title bar (`bun-app-window/titlebar`)
|
|
80
|
+
|
|
81
|
+
- `mountTitleBar()` with `TITLEBAR_GLYPHS` in both `unicode` and `fluent` sets, accessible labels that track the maximized state, drag and double-click-to-maximize, and control-area exclusion so pressing a button does not drag the window.
|
|
82
|
+
- `titleBarCss()` injects the stylesheet once and shares it across instances; themes switch through attribute selectors. `bridgeFromWindowService()` forwards the four window methods.
|
|
83
|
+
- Zero external dependencies and no access to shell internals — the component only needs the DOM and the bridge.
|
|
84
|
+
|
|
85
|
+
#### Placement persistence (`bun-app-window/state`)
|
|
86
|
+
|
|
87
|
+
- `savePlacement` / `loadPlacement` / `capturePlacement` / `restorePlacement` / `attachPlacementStore`, plus `parseBounds` / `parsePlacement` for the raw formats.
|
|
88
|
+
- Writes are atomic (temporary file plus rename, no `.tmp` residue). Reads validate the shape, so a hand-edited, truncated, or missing file degrades to `null` instead of throwing.
|
|
89
|
+
- `attachPlacementStore` polls at 700 ms and does not touch the disk while the window is at rest.
|
|
90
|
+
- Maximized windows persist their restore rectangle; coordinates are converted across DPI changes and corrected when the monitor they were saved on is gone.
|
|
91
|
+
|
|
92
|
+
#### Diagnostics (`bun-app-window/debug`)
|
|
93
|
+
|
|
94
|
+
- `debugOf(shell)` exposes `hittest`, `metrics`, `dpi`, `selfBase`, and `selfPath`. Deliberately kept off the main facade so the diagnostic shape can evolve without becoming part of the public contract.
|
|
95
|
+
|
|
96
|
+
#### Packaging and distribution
|
|
97
|
+
|
|
98
|
+
- `resolveNativeLib()` follows a four-level priority: explicit `nativeLibPath`, embedded bytes released to `%LOCALAPPDATA%\<appName>\runtime\` and named by content hash, a `libshell.dll` next to the executable, then the development layout (`.build/` before `dist/`). The reported `source` distinguishes `explicit` / `embedded` / `package` / `dev`.
|
|
99
|
+
- `bun-app-window/embedded` bundles the DLL into `bun build --compile` output. `embeddedDll()` returns `null` outside a compiled binary rather than throwing.
|
|
100
|
+
- Released copies are written atomically, reused when an existing file already matches by size (`reused: true`), and stale hashed copies are pruned without failing on files still mapped by another process.
|
|
101
|
+
- `serveOnSafePort()` avoids the browser-restricted port list that causes `ERR_UNSAFE_PORT`, binding inside 40000–60000; `isRestrictedPort()` exposes the check.
|
|
102
|
+
- The `bun-app-window/native` subpath resolves to the shipped `dist/libshell.dll`.
|
|
103
|
+
|
|
104
|
+
#### Quality gates
|
|
105
|
+
|
|
106
|
+
- `bun check` type-checks 21 files and `bun test` runs 103 cases across 10 files, covering exports, first paint, capabilities, IPC, the native surface, ports, library resolution, placement, the title bar, and agreement between TypeScript constants and their Win32 counterparts.
|
|
107
|
+
- TypeScript strict mode with `exactOptionalPropertyTypes`, `noUncheckedIndexedAccess`, `verbatimModuleSyntax`, `erasableSyntaxOnly`, `noImplicitOverride`, and `noUnusedLocals` / `noUnusedParameters`.
|
|
108
|
+
- The 33 exported symbols of `libshell.dll` are pinned by a hand-written list in the native surface test, so a symbol cannot silently drop out of coverage.
|
|
109
|
+
|
|
110
|
+
### Known limitations
|
|
111
|
+
|
|
112
|
+
- Windows x64 only. The `os` and `cpu` fields in `package.json` reject every other platform at install time. Cross-platform support is a reservation in the design, not a capability: `shell.cc` is Win32 and WebView2 throughout.
|
|
113
|
+
- TypeScript sources are published, not compiled. Consumers need Bun 1.4.3 or newer. Installing under Node succeeds but nothing runs.
|
|
114
|
+
- `bun:ffi` is marked experimental in Bun, and the entire native boundary depends on it.
|
|
115
|
+
- `pickFolder()` blocks the calling thread. The modal loop inside `SHBrowseForFolder` keeps timers on that thread from firing, and the call cannot be cancelled or timed out. Run it in a worker if you need concurrency.
|
|
116
|
+
- One window per process. The native layer keeps a process-wide singleton, so a second `start()` fails until the previous window is observed dead through `isAlive()`.
|
|
117
|
+
- Not yet verified in managed environments that enforce AppLocker, WDAC, or third-party EDR behaviour blocking.
|
|
118
|
+
- No commercially trusted code signing: only a self-signed certificate has been exercised end to end.
|
|
119
|
+
|
|
120
|
+
[Unreleased]: https://github.com/TiancongLx/bun-app-window/compare/v0.2.0...HEAD
|
|
121
|
+
[0.2.0]: https://github.com/TiancongLx/bun-app-window/compare/v0.1.0...v0.2.0
|
|
122
|
+
[0.1.0]: https://github.com/TiancongLx/bun-app-window/releases/tag/v0.1.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dolores (TiancongLx)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,433 @@
|
|
|
1
|
-
#
|
|
1
|
+
# bun-app-window
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/bun-app-window)
|
|
4
|
+
[](https://bun.sh)
|
|
5
|
+
[](#requirements--design-boundaries)
|
|
6
|
+
[](#requirements--design-boundaries)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
|
|
9
|
+
English | [简体中文](./README.zh-CN.md) | [正體中文](./README.zh-TW.md)
|
|
10
|
+
|
|
11
|
+
A native window shell for Bun applications on Windows, hosting web UIs via the system WebView2 runtime without bundling a browser.
|
|
12
|
+
|
|
13
|
+
This library maintains a strict, minimal scope: it handles window lifecycle, DPI-aware geometry, frameless chrome customization, and the native message channel. Business logic and UI rendering live entirely outside the shell.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { openShell, serveOnSafePort, FLAG_FRAMELESS } from "bun-app-window";
|
|
17
|
+
|
|
18
|
+
const server = serveOnSafePort({ fetch: () => new Response("<h1>Hello</h1>") });
|
|
19
|
+
await openShell({ url: `http://127.0.0.1:${server.port}`, flags: FLAG_FRAMELESS });
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Requirements & Design Boundaries
|
|
25
|
+
|
|
26
|
+
Before adopting this package, please review the following architectural constraints and trade-offs:
|
|
27
|
+
|
|
28
|
+
| Constraint | Implication |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| Windows x64 only | Enforced via `os` and `cpu` fields in `package.json` (the platform-neutral package name is reserved for future expansion). A prebuilt `libshell.dll` is shipped in the package; no install-time compilation is required |
|
|
31
|
+
| Bun runtime required | Ships raw TypeScript source files and requires Bun >= 1.4.3. While Node.js can install the package, it cannot execute it |
|
|
32
|
+
| Type-checking needs `@types/bun` | Because the package ships `.ts` sources, your own `bun check` compiles them too. Add `bun add -d @types/bun`; without it the checker reports `TS2868: Cannot find name 'Bun'` from inside `node_modules` |
|
|
33
|
+
| Built on `bun:ffi` | `bun:ffi` is marked as experimental in Bun. While Bun recommends Node-API for production stability, this library chooses `bun:ffi` to achieve a zero-dependency footprint |
|
|
34
|
+
|
|
35
|
+
Runtime prerequisites and non-goals:
|
|
36
|
+
|
|
37
|
+
- System WebView2 Runtime: Requires the evergreen WebView2 Runtime installed on the host machine (pre-installed on Windows 11).
|
|
38
|
+
- Interactive desktop session: Due to Windows Session 0 isolation, WebView2 cannot run inside non-interactive background services. However, running inside elevated (High Integrity Level) administrator processes is fully supported.
|
|
39
|
+
- Explicit non-goals: System tray icons, native application menu bars, auto-updaters, multi-window orchestration, and cross-platform abstractions.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
bun add bun-app-window
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### What the tarball contains
|
|
48
|
+
|
|
49
|
+
The published package holds the prebuilt `dist/libshell.dll` plus the TypeScript sources — 19 files, ~0.36 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
|
+
|
|
51
|
+
## Quick Start
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { openShell, serveOnSafePort, FLAG_FRAMELESS, FLAG_CENTER } from "bun-app-window";
|
|
55
|
+
|
|
56
|
+
const server = serveOnSafePort({
|
|
57
|
+
fetch: () => new Response("<h1>Hello</h1>", {
|
|
58
|
+
headers: { "content-type": "text/html; charset=utf-8" },
|
|
59
|
+
}),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const shell = await openShell({
|
|
63
|
+
url: `http://127.0.0.1:${server.port}`,
|
|
64
|
+
title: "My App",
|
|
65
|
+
width: 1000,
|
|
66
|
+
height: 680,
|
|
67
|
+
flags: FLAG_FRAMELESS | FLAG_CENTER,
|
|
68
|
+
minWidth: 480,
|
|
69
|
+
minHeight: 320,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
shell.post(JSON.stringify({ t: "ready" })); // Host -> Page one-way message
|
|
73
|
+
shell.setTitle("Doing work…");
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Note: `createShell(opts)` only resolves and loads the native DLL without opening a window. `openShell(opts)` creates the instance, starts the window immediately, and throws an error on failure.
|
|
77
|
+
|
|
78
|
+
## Core API
|
|
79
|
+
|
|
80
|
+
### Instance Methods & Properties
|
|
81
|
+
|
|
82
|
+
| Member | Description |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `start()` | Opens the window and returns `{ rc, error }`. Returns immediately while the UI message loop runs on a dedicated native thread |
|
|
85
|
+
| `stop()` | Destroys the window and releases native resources. Safe to call multiple times (idempotent) |
|
|
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) |
|
|
88
|
+
| `state()` | Returns current window state: `{ maximized, minimized, focused, dark, raw }` |
|
|
89
|
+
| `lastError()` | Returns the most recent failure message, or an empty string if no error occurred |
|
|
90
|
+
| `setTitle` / `minimize` / `toggleMaximize` / `close` | Standard window control operations; each returns a status code `rc` |
|
|
91
|
+
| `setMinSize(w, h)` | Sets the minimum window dimensions in logical pixels |
|
|
92
|
+
| `drag()` | Initiates native window dragging from a page `pointerdown` event (preserves Windows Snap Layouts) |
|
|
93
|
+
| `beginResize(edge)` | Initiates native border resizing from a page `pointermove` event (`edge` uses `RESIZE_EDGE.*` constants) |
|
|
94
|
+
| `post(json)` | Sends a fire-and-forget JSON message from the host to the web page |
|
|
95
|
+
| `setZoom(f)` / `getZoom()` | Gets or sets the page zoom factor (clamped automatically to `0.25`–`3.0`) |
|
|
96
|
+
| `reload()` / `openDevTools()` / `toggleFullscreen()` | Equivalent to `Ctrl+R`, `F12`, and `F11` respectively |
|
|
97
|
+
| `setBrowserUi(mask)` | Disables specific default browser behaviors using a bitwise OR of `BROWSER_UI_*` flags |
|
|
98
|
+
| `showContextMenu(x, y, items)` | Displays a native Win32 context menu at the specified screen coordinates (see below) |
|
|
99
|
+
| `pickFolder(title?)` | Opens a native folder selection dialog (synchronously blocks the calling thread) |
|
|
100
|
+
| `libInfo` | Metadata of the resolved native library (file path, resolution source, SHA-256 hash) |
|
|
101
|
+
| `symbols` | Escape hatch providing direct access to the raw `dlopen` FFI symbols |
|
|
102
|
+
|
|
103
|
+
### Window Flags
|
|
104
|
+
|
|
105
|
+
Combine via bitwise OR (`|`) and pass to `opts.flags`:
|
|
106
|
+
|
|
107
|
+
| Flag | Description |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| `FLAG_FRAMELESS` | Creates a frameless window without the default system title bar, delegating title bar rendering to the web UI |
|
|
110
|
+
| `FLAG_RESIZABLE` | Allows user resizing |
|
|
111
|
+
| `FLAG_CENTER` | Centers the window within the active monitor work area on startup (works in both standard and frameless modes) |
|
|
112
|
+
| `FLAG_TOPMOST` | Keeps the window always on top |
|
|
113
|
+
| `FLAG_DEFER_SHOW` | Creates the window hidden initially until `show()` is explicitly called |
|
|
114
|
+
| `FLAG_ACCEPT_DROP` | Enables native file and folder drag-and-drop from Windows Explorer |
|
|
115
|
+
| `FLAG_START_MAXIMIZED` | Opens the window directly in a maximized state without an intermediate normal-sized frame flash |
|
|
116
|
+
|
|
117
|
+
## Subpath Exports
|
|
118
|
+
|
|
119
|
+
Additional capabilities outside core window management are modularized into dedicated subpath exports.
|
|
120
|
+
|
|
121
|
+
### `bun-app-window/ipc` — Typed RPC & Event Bus
|
|
122
|
+
|
|
123
|
+
Share a single TypeScript contract between the host and the web page for compile-time verification of method signatures and event payloads:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
// api.ts — Imported by both host and page
|
|
127
|
+
export type Api = { greet(name: string): string };
|
|
128
|
+
export type Events = { windowState: { maximized: boolean } };
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// Host
|
|
133
|
+
import { createIpcServer } from "bun-app-window/ipc";
|
|
134
|
+
|
|
135
|
+
const ipc = createIpcServer({
|
|
136
|
+
service: { greet: (name: string) => `Hello ${name}` },
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
serveOnSafePort({
|
|
140
|
+
fetch(req) { /* Serve assets & authenticate */ },
|
|
141
|
+
websocket: {
|
|
142
|
+
open: (ws) => ipc.attach(ws),
|
|
143
|
+
message: (ws, raw) => ipc.handle(ws, raw),
|
|
144
|
+
close: (ws) => ipc.detach(ws),
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
ipc.emit("windowState", { maximized: false }); // Push event to connected pages
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
// Page
|
|
153
|
+
import { connect } from "bun-app-window/ipc";
|
|
154
|
+
import type { Api, Events } from "./api";
|
|
155
|
+
|
|
156
|
+
const api = await connect<Api, Events>();
|
|
157
|
+
await api.greet("Ada"); // Inferred return type: string
|
|
158
|
+
api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
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.
|
|
162
|
+
|
|
163
|
+
### `bun-app-window/titlebar` — Windows 11 Title Bar
|
|
164
|
+
|
|
165
|
+
Mounts a customizable Windows 11 title bar inside frameless windows:
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
// Page
|
|
169
|
+
import { mountTitleBar, bridgeFromWindowService } from "bun-app-window/titlebar";
|
|
170
|
+
|
|
171
|
+
const bar = mountTitleBar({
|
|
172
|
+
title: "My App",
|
|
173
|
+
icon: "/icon.svg",
|
|
174
|
+
bridge: bridgeFromWindowService(api.window),
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
api.on("windowState", (s) => bar.setMaximized(s.maximized));
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
Expose the four corresponding window control methods on the host IPC service:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
// Host
|
|
184
|
+
createIpcServer({
|
|
185
|
+
service: {
|
|
186
|
+
window: {
|
|
187
|
+
minimize: () => shell.minimize(),
|
|
188
|
+
toggleMaximize: () => shell.toggleMaximize(),
|
|
189
|
+
close: () => shell.close(),
|
|
190
|
+
drag: () => shell.drag(),
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Implementation notes:
|
|
197
|
+
|
|
198
|
+
- Native dragging: Calling `shell.drag()` dispatches a native `WM_NCLBUTTONDOWN` (`HTCAPTION`) message, preserving Windows 11 Snap Layouts and double-click maximize behavior.
|
|
199
|
+
- Theming & icons: Colors and dimensions are controlled via CSS custom properties (`--ws-tb-*`). Window control buttons default to geometric glyphs for universal compatibility; setting `icons: "fluent"` switches to `Segoe Fluent Icons` (note: missing fonts on older systems will render as missing-glyph boxes).
|
|
200
|
+
|
|
201
|
+
### `bun-app-window/state` — Window Placement Persistence
|
|
202
|
+
|
|
203
|
+
Automatically persists and restores window coordinates, dimensions, and maximized state across sessions:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
import { attachPlacementStore, loadPlacement, restorePlacement } from "bun-app-window/state";
|
|
207
|
+
|
|
208
|
+
const FILE = "C:\\Users\\me\\AppData\\Roaming\\MyApp\\window.json";
|
|
209
|
+
const shell = await openShell({ url, flags: FLAG_DEFER_SHOW });
|
|
210
|
+
|
|
211
|
+
const saved = await loadPlacement(FILE);
|
|
212
|
+
if (saved) restorePlacement(shell, saved);
|
|
213
|
+
|
|
214
|
+
shell.show();
|
|
215
|
+
|
|
216
|
+
const store = attachPlacementStore(shell, FILE); // Polls state; skips disk writes when idle
|
|
217
|
+
process.on("beforeExit", () => store.flush());
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
The native placement layer handles edge cases automatically:
|
|
221
|
+
|
|
222
|
+
- Captures the normal restore rectangle (`rcNormalPosition`) when maximized rather than full-screen bounds.
|
|
223
|
+
- Translates coordinates across monitors with different DPI scaling factors.
|
|
224
|
+
- Validates and clamps off-screen coordinates when monitor topology changes.
|
|
225
|
+
|
|
226
|
+
For one-off reads and writes without polling timers, use `capturePlacement` and `savePlacement` directly.
|
|
227
|
+
|
|
228
|
+
### `bun-app-window/debug` — Diagnostics
|
|
229
|
+
|
|
230
|
+
Exposes internal metrics and hit-testing utilities. Note: This module is excluded from semver stability guarantees and may change at any time.
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import { debugOf } from "bun-app-window/debug";
|
|
234
|
+
|
|
235
|
+
const d = debugOf(shell);
|
|
236
|
+
d.metrics(); // Returns pos / win / client / work / border / round geometry
|
|
237
|
+
d.dpi(); // Returns DPI awareness level and scaling factor
|
|
238
|
+
d.hittest(x, y);
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### `bun-app-window/native` — The Prebuilt DLL
|
|
242
|
+
|
|
243
|
+
Resolves to `dist/libshell.dll` itself, so `import.meta.resolve()` hands you a real path you can copy next to your executable:
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
import nativeLibPath from "bun-app-window/native" with { type: "file" };
|
|
247
|
+
|
|
248
|
+
await Bun.write(join(dirname(process.execPath), "libshell.dll"), Bun.file(nativeLibPath));
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The `with { type: "file" }` attribute is required — without it the bundler tries to parse the DLL as a module. This subpath is for hand-rolled packaging; if you use `bun build --compile`, `bun-app-window/embedded` does the same job without touching the filesystem.
|
|
252
|
+
|
|
253
|
+
## Common Recipes & Best Practices
|
|
254
|
+
|
|
255
|
+
### 1. Disabling Browser-Specific UI
|
|
256
|
+
|
|
257
|
+
WebView2 enables standard browser interactions by default. Desktop applications typically disable all of them:
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
import { BROWSER_UI_ALL } from "bun-app-window";
|
|
261
|
+
|
|
262
|
+
const shell = await openShell({ url, browserUi: BROWSER_UI_ALL });
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Individual flags can also be combined via bitwise OR:
|
|
266
|
+
|
|
267
|
+
| Flag | Disabled Behavior |
|
|
268
|
+
| --- | --- |
|
|
269
|
+
| `BROWSER_UI_MENU` | Default Edge right-click context menu |
|
|
270
|
+
| `BROWSER_UI_KEYS` | Built-in browser shortcuts (`Ctrl+P`, `Ctrl+F`, `Ctrl+S`) |
|
|
271
|
+
| `BROWSER_UI_STATUSBAR` | Bottom-left URL hover status bar |
|
|
272
|
+
| `BROWSER_UI_SWIPE` | Trackpad horizontal swipe back/forward navigation |
|
|
273
|
+
| `BROWSER_UI_AUTOFILL` | Form autofill popups |
|
|
274
|
+
|
|
275
|
+
Note: `F11`, `F12`, and `Ctrl±` zoom shortcuts are handled directly by the native shell and remain active regardless of these flags.
|
|
276
|
+
|
|
277
|
+
### 2. Native Context Menu
|
|
278
|
+
|
|
279
|
+
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:
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
// Host: expose the menu capability on the window service
|
|
283
|
+
createIpcServer({
|
|
284
|
+
service: {
|
|
285
|
+
window: {
|
|
286
|
+
showContextMenu: (x: number, y: number, items: string) =>
|
|
287
|
+
shell.showContextMenu(x, y, items),
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
```ts
|
|
294
|
+
// Page: trigger the native menu
|
|
295
|
+
document.addEventListener("contextmenu", async (e) => {
|
|
296
|
+
e.preventDefault();
|
|
297
|
+
await api.window.showContextMenu(
|
|
298
|
+
Math.round(e.screenX),
|
|
299
|
+
Math.round(e.screenY),
|
|
300
|
+
"open|Open\nrefresh|Refresh\n-\nquit|Quit"
|
|
301
|
+
);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// Page: handle menu item selection
|
|
305
|
+
window.addEventListener("shell-msg", (e) => {
|
|
306
|
+
if (e.detail.t === "menu") console.log("Selected ID:", e.detail.id);
|
|
307
|
+
});
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Menu items are specified as a newline-delimited string: `id|Label` per line, or `-` for a separator line.
|
|
311
|
+
|
|
312
|
+
### 3. File Drag and Drop
|
|
313
|
+
|
|
314
|
+
Pass `FLAG_ACCEPT_DROP` to receive file and folder drops from Windows Explorer:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
// Host
|
|
318
|
+
import { openShell, FLAG_ACCEPT_DROP } from "bun-app-window";
|
|
319
|
+
|
|
320
|
+
const shell = await openShell({ url, flags: FLAG_ACCEPT_DROP });
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
```js
|
|
324
|
+
// Page
|
|
325
|
+
window.addEventListener("shell-msg", (e) => {
|
|
326
|
+
if (e.detail.t === "drop") console.log("Dropped paths:", e.detail.paths);
|
|
327
|
+
});
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
When running in an elevated administrator process (High Integrity Level) while Explorer runs at Medium Integrity Level, the shell automatically whitelists drop messages across UIPI via `ChangeWindowMessageFilterEx`.
|
|
331
|
+
|
|
332
|
+
### 4. Recommended Desktop CSS Reset
|
|
333
|
+
|
|
334
|
+
The shell does not inject any implicit DOM nodes or stylesheets into your page. To make your web UI feel like a native desktop app, include the following baseline rules:
|
|
335
|
+
|
|
336
|
+
```css
|
|
337
|
+
html, body { cursor: default; }
|
|
338
|
+
body { user-select: none; }
|
|
339
|
+
input, textarea, .log { user-select: text; cursor: auto; }
|
|
340
|
+
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
```html
|
|
344
|
+
<input type="text" spellcheck="false" autocomplete="off" autocapitalize="off">
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Setting `spellcheck="false"` is particularly important—red squiggly underlines beneath file paths and numeric inputs immediately give away a web view.
|
|
348
|
+
|
|
349
|
+
### 5. Compiling to a Standalone Executable (.exe)
|
|
350
|
+
|
|
351
|
+
When bundling with `bun build --compile`, import `bun-app-window/embedded` to embed `libshell.dll` directly inside the single-file executable:
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
import { createShell } from "bun-app-window";
|
|
355
|
+
import { embeddedDll } from "bun-app-window/embedded"; // Embeds the DLL into the binary
|
|
356
|
+
|
|
357
|
+
const shell = await createShell({ url, embedded: embeddedDll });
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
```bash
|
|
361
|
+
bun build --compile --windows-hide-console src/index.ts --outfile dist/app.exe
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
Tips:
|
|
365
|
+
|
|
366
|
+
- A side-effect import `import "bun-app-window/embedded";` also works automatically.
|
|
367
|
+
- Always pass `--windows-hide-console` for GUI builds; omitting it will spawn a black command prompt window alongside your application.
|
|
368
|
+
|
|
369
|
+
## Internals & Reference
|
|
370
|
+
|
|
371
|
+
### Native Library Resolution Order
|
|
372
|
+
|
|
373
|
+
At runtime, `libshell.dll` is located in the following order:
|
|
374
|
+
|
|
375
|
+
1. Explicit path: `opts.nativeLibPath`, bypassing all fallback heuristics.
|
|
376
|
+
2. Embedded payload: Extracted to `%LOCALAPPDATA%\<appName>\runtime` named by content hash (stale versions are automatically pruned).
|
|
377
|
+
3. Executable directory: `libshell.dll` located next to the running `.exe`.
|
|
378
|
+
4. Development paths: `.build/libshell.dll` followed by `dist/libshell.dll` relative to the package root.
|
|
379
|
+
|
|
380
|
+
### Built-in Keyboard & Mouse Shortcuts
|
|
381
|
+
|
|
382
|
+
| Input | Behavior |
|
|
383
|
+
| --- | --- |
|
|
384
|
+
| `Ctrl +` / `Ctrl -` / `Ctrl 0` | Zoom in / Zoom out / Reset zoom to 100% |
|
|
385
|
+
| `Ctrl + Mouse Wheel` | Zoom in / Zoom out |
|
|
386
|
+
| `F11` | Toggle fullscreen mode (hides window borders and covers the taskbar) |
|
|
387
|
+
| `F5` / `Ctrl + R` | Reload page |
|
|
388
|
+
| `F12` | Open WebView2 Developer Tools |
|
|
389
|
+
| `Alt + Space` / Right-click title bar | Open native Windows system menu |
|
|
390
|
+
|
|
391
|
+
## Development & Verification
|
|
392
|
+
|
|
393
|
+
Built with Bun 1.4.3 and TypeScript 7.0.2 with full strict type-checking enabled.
|
|
394
|
+
|
|
395
|
+
**This project has no remote CI.** GitHub Actions cannot allocate runners for this account (billing lock), and the project is a single-platform, single-developer package with no external service dependencies — so the gates run locally instead. The recommended entry point is:
|
|
396
|
+
|
|
397
|
+
```powershell
|
|
398
|
+
bun run scripts/gate.ts # bun check + bun test + consumer acceptance
|
|
399
|
+
bun run scripts/gate.ts --clean # same gates, but inside a fresh clone of HEAD
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
`-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.
|
|
403
|
+
|
|
404
|
+
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 — 40 assertions covering every subpath, the DLL actually being `dlopen`-ed out of `node_modules`, and `libInfo.source === "package"`. 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.
|
|
405
|
+
|
|
406
|
+
The underlying commands remain available directly:
|
|
407
|
+
|
|
408
|
+
```bash
|
|
409
|
+
bun check # Run TypeScript type checker
|
|
410
|
+
bun test # Run automated test suite (tests/, 182 test cases)
|
|
411
|
+
bun run demo # Launch interactive self-test demo (requires desktop session)
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Note that `bun run demo` and the compiled-executable acceptance (shape B in `AGENTS.md`) both need an interactive desktop, so they were never CI-capable — they are local-only by construction.
|
|
415
|
+
|
|
416
|
+
Whenever `src/shell/` or `package.json` is modified, the consumer acceptance stage already covers the installed-package path (shape A in `AGENTS.md`); it runs as part of the gate, so there is nothing to remember. Only shape B — building the single-file `.exe` — still needs a manual run inside a desktop session.
|
|
417
|
+
|
|
418
|
+
For window enumeration (`EnumWindows`) or automated screenshot scripts, filter by the corresponding Win32 window class name:
|
|
419
|
+
|
|
420
|
+
| Window Mode | Window Class Name |
|
|
421
|
+
| --- | --- |
|
|
422
|
+
| Standard framed (without `FLAG_FRAMELESS`) | `webview` |
|
|
423
|
+
| Frameless (with `FLAG_FRAMELESS`) | `webview_shell` |
|
|
424
|
+
|
|
425
|
+
## Acknowledgments
|
|
426
|
+
|
|
427
|
+
- 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.
|
|
428
|
+
- WebView2 SDK provided by [Microsoft.Web.WebView2](https://www.nuget.org/packages/Microsoft.Web.WebView2) `1.0.1150.38` (NuGet package vendored for offline builds). The WebView2 Runtime is provided by the OS.
|
|
429
|
+
- Co-authored-by DeepSeek
|
|
430
|
+
|
|
431
|
+
## License
|
|
432
|
+
|
|
433
|
+
Released under the [MIT License](./LICENSE). `vendor/webview/` is also MIT-licensed with original copyright notices retained in source files.
|