|
| 1 | +# Consuming `@nativescript/windows` from Node, Bun, and Deno |
| 2 | + |
| 3 | +The package is a napi-rs native addon (`windows.<triple>.node`) plus a small JS layer |
| 4 | +(`index.js` from napi-rs, and `nswinrt.js` with the WinRT ergonomics: `toPromise`, auto-pump, |
| 5 | +`interop.*`). Because it targets the **Node-API** ABI, any runtime that implements Node-API can |
| 6 | +load it — the engine underneath differs, but the interop code does not. |
| 7 | + |
| 8 | +## Loading |
| 9 | + |
| 10 | +### Node.js (V8) |
| 11 | +```js |
| 12 | +const { Windows, toPromise, enableAutoPump } = require('@nativescript/windows/nswinrt.js'); |
| 13 | +enableAutoPump(); |
| 14 | +``` |
| 15 | +Or ESM via `createRequire`. No flags needed. This is the primary, fully-tested path. |
| 16 | + |
| 17 | +### Bun (JavaScriptCore) |
| 18 | +Bun implements Node-API and loads `.node` addons through the same `require`/`import`: |
| 19 | +```js |
| 20 | +import { Windows, toPromise } from '@nativescript/windows/nswinrt.js'; |
| 21 | +``` |
| 22 | +This is the **JSC-on-Windows** route — no WebKit embedding needed; Bun provides JSC + Node-API. |
| 23 | + |
| 24 | +### Deno (V8) |
| 25 | +Deno implements Node-API behind its Node-compat layer. Import via the `npm:` specifier (or |
| 26 | +`createRequire`), and grant the permissions the addon needs: |
| 27 | +``` |
| 28 | +deno run --allow-ffi --allow-read --allow-env --allow-write app.js |
| 29 | +``` |
| 30 | +```js |
| 31 | +import { Windows, toPromise } from "npm:@nativescript/windows/nswinrt.js"; |
| 32 | +``` |
| 33 | +`--allow-ffi` is required for native addons; `--allow-read` is needed for winmd discovery |
| 34 | +(below). |
| 35 | + |
| 36 | +> The native `.node` is identical across all three — only the host engine changes. `nswinrt.js` |
| 37 | +> is plain CommonJS and uses only `require('./index.js')` + timers, so it runs unmodified on all |
| 38 | +> three runtimes. |
| 39 | +
|
| 40 | +## winmd discovery (the important part) |
| 41 | + |
| 42 | +WinRT type metadata comes from two sources: |
| 43 | + |
| 44 | +1. **System / OS types** (`Windows.*`) — resolved by the OS via `RoGetMetaDataFile`. **No files |
| 45 | + ship with the app**; these always resolve on any Windows machine, in any runtime. Everything |
| 46 | + in the test suite (JsonObject, Uri, Calendar, ThreadPool, CryptographicBuffer, …) is system |
| 47 | + metadata and needs zero configuration. |
| 48 | + |
| 49 | +2. **Third-party / app `.winmd`** (WebView2, your own WinRT components) — **not** discoverable by |
| 50 | + the OS; they must be registered explicitly. |
| 51 | + |
| 52 | +### How the napi package finds them |
| 53 | + |
| 54 | +The rusty_v8 runtime auto-scans for `.winmd` in `Runtime::new` (exe dir + app root). **The napi |
| 55 | +path never constructs a `Runtime`** — it drives interop directly on the host's `napi_env` — so |
| 56 | +that scan is reproduced on the napi side: |
| 57 | + |
| 58 | +- **Automatic:** the first `getNamespace(...)` (or any interop call) runs a one-time scan of the |
| 59 | + **current working directory** and the **host executable's directory** |
| 60 | + (`scan_default_winmd_dirs`). Drop your `.winmd` next to where you launch and it's picked up. |
| 61 | +- **Explicit (recommended for apps):** point the runtime at your metadata directly — |
| 62 | + ```js |
| 63 | + const { interop } = require('@nativescript/windows/nswinrt.js'); |
| 64 | + interop.registerWinmd('C:/app/metadata/MyComponent.winmd'); // one file |
| 65 | + interop.scanWinmdDir('C:/app/metadata'); // whole folder → count |
| 66 | + ``` |
| 67 | + Call these **before** touching the corresponding namespaces. |
| 68 | + |
| 69 | +### Why auto-scan differs from the standalone runtime |
| 70 | + |
| 71 | +Under the standalone (embedded-V8) runtime, `current_exe` is *your app's* exe, so the exe-dir |
| 72 | +scan finds app-bundled winmd automatically. Under Node/Bun/Deno, `current_exe` is |
| 73 | +`node.exe`/`bun.exe`/`deno.exe` — so the exe-dir scan covers the runtime's own directory, and the |
| 74 | +**cwd scan + explicit `registerWinmd`/`scanWinmdDir`** are how app metadata gets loaded. A CLI |
| 75 | +that launches apps should either `chdir` to the metadata location or call `scanWinmdDir` on |
| 76 | +startup. (This matches the NativeScript CLI's existing responsibility to deploy the `.winmd`.) |
| 77 | + |
| 78 | +## The message loop / async |
| 79 | + |
| 80 | +WinRT async completions (`IAsyncAction`/`IAsyncOperation`) are delivered on the STA thread's |
| 81 | +message queue, so that queue must be pumped for a `Promise` to settle. `nswinrt.js` handles this: |
| 82 | + |
| 83 | +- `enableAutoPump()` — installs a ref-counted pump on the host timer loop; it's ref'd only while |
| 84 | + a WinRT `Promise` is outstanding (so the process still exits normally). After this, plain |
| 85 | + `await toPromise(op)` just works. |
| 86 | +- `awaitWithPump(promise, timeoutMs)` — explicit alternative that pumps inline until settle. |
| 87 | + |
| 88 | +This is the same on all three runtimes (all expose `setInterval`/`setImmediate`). |
| 89 | + |
| 90 | +## Natural-syntax extras |
| 91 | + |
| 92 | +Beyond method/property/event access, the napi backend supports: |
| 93 | + |
| 94 | +- **Keyed maps (`IMap`/`IMapView`/`IPropertySet`)** — classes whose default interface is a map |
| 95 | + (`StringMap`, `PropertySet`, `ValueSet`, `ApplicationDataContainerSettings`, …) and |
| 96 | + interface-typed map returns get keyed sugar: `m['key']` → `Lookup`, `m['key'] = v` → `Insert`, |
| 97 | + `'key' in m` → `HasKey`, plus `m.length` → `Size`. WinRT member names always win over map |
| 98 | + keys (use `m.Lookup('Size')` for a shadowed key). `PropertySet`/`ValueSet` values box on |
| 99 | + insert and unbox (`IPropertyValue` primitives) on lookup, so `ps['n'] = 42; ps['n'] === 42`. |
| 100 | + Classes that merely *also* implement `IMap` alongside a richer identity (e.g. `JsonObject`) |
| 101 | + stay host objects — call `Lookup`/`Insert` explicitly there. |
| 102 | +- **Subclassing** — `class Sub extends WinRTClass` works on both object models (host objects |
| 103 | + and Proxy-path instances): constructor args flow through `super(...)`, overrides can call |
| 104 | + `super.Method()`, instances satisfy `instanceof` for both the subclass and the WinRT class, |
| 105 | + and subclass instances marshal as WinRT arguments (identity-cached, so the same JS object |
| 106 | + comes back out). |
| 107 | +- **Composable (non-sealed) constructors** — the composition (null-outer) ABI is wired; note |
| 108 | + every activatable non-sealed WinRT class lives in `Windows.UI.Xaml`, whose activation |
| 109 | + requires a XAML-initialized thread. Headless those ctors surface the factory's |
| 110 | + `RPC_E_WRONG_THREAD` as a normal catchable JS error (identical to the classic runtime). |
| 111 | + `Windows.UI.Composition` object trees (non-sealed bases) work headless — the backend creates |
| 112 | + a `DispatcherQueue` for the JS thread at init. |
| 113 | +- **`setImmediate`/`clearImmediate`** — provided by the standalone engine hosts' event loop |
| 114 | + (Node/Bun/Deno already have their own). |
0 commit comments