@sublime-ui/desktop 0.1.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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +47 -0
  3. package/dist/bridge/main-router.d.ts +30 -0
  4. package/dist/bridge/main-router.js +22 -0
  5. package/dist/bridge/preload.d.ts +32 -0
  6. package/dist/bridge/preload.js +9 -0
  7. package/dist/bridge/proxy.d.ts +24 -0
  8. package/dist/bridge/proxy.js +10 -0
  9. package/dist/client.d.ts +5 -0
  10. package/dist/client.js +16 -0
  11. package/dist/define-native.d.ts +18 -0
  12. package/dist/define-native.js +6 -0
  13. package/dist/errors.d.ts +24 -0
  14. package/dist/errors.js +29 -0
  15. package/dist/index.d.ts +16 -0
  16. package/dist/index.js +46 -0
  17. package/dist/registry.d.ts +22 -0
  18. package/dist/registry.js +21 -0
  19. package/dist/services/clipboard.d.ts +16 -0
  20. package/dist/services/clipboard.js +15 -0
  21. package/dist/services/dialog.d.ts +19 -0
  22. package/dist/services/dialog.js +23 -0
  23. package/dist/services/fs.d.ts +20 -0
  24. package/dist/services/fs.js +26 -0
  25. package/dist/services/get-electron.d.ts +14 -0
  26. package/dist/services/get-electron.js +6 -0
  27. package/dist/services/index.d.ts +7 -0
  28. package/dist/services/index.js +12 -0
  29. package/dist/services/notifications.d.ts +21 -0
  30. package/dist/services/notifications.js +11 -0
  31. package/dist/services/shell.d.ts +16 -0
  32. package/dist/services/shell.js +19 -0
  33. package/dist/shell/create-window.d.ts +48 -0
  34. package/dist/shell/create-window.js +28 -0
  35. package/dist/shell/main.d.ts +53 -0
  36. package/dist/shell/main.js +18 -0
  37. package/dist/types.d.ts +16 -0
  38. package/dist/types.js +0 -0
  39. package/dist/use-native.d.ts +24 -0
  40. package/dist/use-native.js +21 -0
  41. package/package.json +35 -0
  42. package/src/bridge/main-router.ts +52 -0
  43. package/src/bridge/preload.ts +43 -0
  44. package/src/bridge/proxy.ts +31 -0
  45. package/src/client.ts +33 -0
  46. package/src/define-native.ts +21 -0
  47. package/src/errors.ts +45 -0
  48. package/src/index.ts +68 -0
  49. package/src/registry.ts +44 -0
  50. package/src/services/clipboard.ts +21 -0
  51. package/src/services/dialog.ts +31 -0
  52. package/src/services/fs.ts +32 -0
  53. package/src/services/get-electron.ts +14 -0
  54. package/src/services/index.ts +13 -0
  55. package/src/services/notifications.ts +24 -0
  56. package/src/services/shell.ts +24 -0
  57. package/src/shell/create-window.ts +75 -0
  58. package/src/shell/main.ts +70 -0
  59. package/src/types.ts +16 -0
  60. package/src/use-native.ts +57 -0
@@ -0,0 +1,19 @@
1
+ import { defineNative } from "../define-native";
2
+ import { getElectron } from "./get-electron";
3
+ const shell = defineNative("shell", {
4
+ openExternal: async (url) => {
5
+ const { shell: s } = await getElectron();
6
+ await s.openExternal(url);
7
+ },
8
+ openPath: async (path) => {
9
+ const { shell: s } = await getElectron();
10
+ await s.openPath(path);
11
+ },
12
+ showItemInFolder: async (path) => {
13
+ const { shell: s } = await getElectron();
14
+ s.showItemInFolder(path);
15
+ }
16
+ });
17
+ export {
18
+ shell
19
+ };
@@ -0,0 +1,48 @@
1
+ import { BrowserWindow } from 'electron';
2
+
3
+ /**
4
+ * Secure `BrowserWindow` factory.
5
+ *
6
+ * Constructs an Electron window with the hardened defaults the bridge relies
7
+ * on — `contextIsolation: true`, `nodeIntegration: false`, and the given
8
+ * preload — so the renderer can only reach native functionality through the
9
+ * registry-validated `native:invoke` router. The `BrowserWindow` constructor
10
+ * is injectable so unit tests can assert the wiring without launching Electron;
11
+ * in production the real constructor is resolved from the `electron` runtime
12
+ * (this runs in the main process, where requiring `electron` is permitted).
13
+ *
14
+ * `entry` is loaded via `loadURL` when it is an `http(s)` URL (dev server) and
15
+ * via `loadFile` otherwise (packaged HTML on disk).
16
+ */
17
+
18
+ /** Minimal `BrowserWindow` surface the factory drives. */
19
+ interface BrowserWindowLike {
20
+ loadURL(url: string): unknown;
21
+ loadFile(file: string): unknown;
22
+ }
23
+ /** Constructor signature for an injectable `BrowserWindow`. */
24
+ type BrowserWindowCtor = new (opts: {
25
+ webPreferences: {
26
+ contextIsolation: boolean;
27
+ nodeIntegration: boolean;
28
+ preload: string;
29
+ };
30
+ }) => BrowserWindow;
31
+ /** Options for {@link createWindow}. */
32
+ interface CreateWindowOptions {
33
+ /** URL (dev server) or file path (packaged) to load. */
34
+ entry: string;
35
+ /** Absolute path to the preload script. */
36
+ preload: string;
37
+ /** Injectable `BrowserWindow` constructor; defaults to Electron's. */
38
+ BrowserWindowCtor?: BrowserWindowCtor;
39
+ }
40
+ /**
41
+ * Construct a hardened `BrowserWindow` and load `entry`.
42
+ *
43
+ * @param opts Window options; `BrowserWindowCtor` is injectable for tests.
44
+ * @returns The constructed window.
45
+ */
46
+ declare function createWindow(opts: CreateWindowOptions): BrowserWindow;
47
+
48
+ export { type BrowserWindowCtor, type BrowserWindowLike, type CreateWindowOptions, createWindow };
@@ -0,0 +1,28 @@
1
+ import { createRequire } from "node:module";
2
+ function resolveCtor() {
3
+ const require2 = createRequire(import.meta.url);
4
+ const electron = require2("electron");
5
+ return electron.BrowserWindow;
6
+ }
7
+ function isUrl(entry) {
8
+ return /^https?:\/\//.test(entry);
9
+ }
10
+ function createWindow(opts) {
11
+ const Ctor = opts.BrowserWindowCtor ?? resolveCtor();
12
+ const win = new Ctor({
13
+ webPreferences: {
14
+ contextIsolation: true,
15
+ nodeIntegration: false,
16
+ preload: opts.preload
17
+ }
18
+ });
19
+ if (isUrl(opts.entry)) {
20
+ win.loadURL(opts.entry);
21
+ } else {
22
+ win.loadFile(opts.entry);
23
+ }
24
+ return win;
25
+ }
26
+ export {
27
+ createWindow
28
+ };
@@ -0,0 +1,53 @@
1
+ import { BrowserWindowCtor } from './create-window.js';
2
+ import { IpcMainLike } from '../bridge/main-router.js';
3
+ import 'electron';
4
+ import '../errors.js';
5
+
6
+ /**
7
+ * Electron application entry for the native bridge.
8
+ *
9
+ * `startDesktop` is the single bootstrap the packaged main process calls: once
10
+ * Electron's `app` reports ready it installs the `native:invoke` router onto
11
+ * `ipcMain` and opens the hardened {@link createWindow}. `app` and `ipcMain`
12
+ * are injected so unit tests can drive the flow with fakes (an
13
+ * immediately-resolving `whenReady`, a recording `ipcMain`) without launching
14
+ * Electron. `isDev` selects how the entry is loaded — a dev server URL via
15
+ * `loadURL` versus packaged HTML on disk via `loadFile`, which `createWindow`
16
+ * derives from the `entry` shape.
17
+ */
18
+
19
+ /** Minimal Electron `app` surface needed to bootstrap. Injectable. */
20
+ interface AppLike {
21
+ whenReady(): Promise<unknown>;
22
+ }
23
+ /** Options for {@link startDesktop}. */
24
+ interface StartDesktopOptions {
25
+ /** Electron's `app` (or a compatible fake for tests). */
26
+ app: AppLike;
27
+ /** Electron's `ipcMain` (or a compatible fake for tests). */
28
+ ipcMain: IpcMainLike;
29
+ /** URL (dev) or file path (packaged) to load in the window. */
30
+ entry: string;
31
+ /** Absolute path to the preload script. */
32
+ preload: string;
33
+ /** Whether running against a dev server. */
34
+ isDev: boolean;
35
+ /** Injectable `BrowserWindow` constructor; defaults to Electron's. */
36
+ BrowserWindowCtor?: BrowserWindowCtor;
37
+ /**
38
+ * Called if `whenReady` rejects or window creation throws. Defaults to
39
+ * logging the error, so a failed bootstrap never becomes an unhandled
40
+ * promise rejection.
41
+ */
42
+ onError?: (error: unknown) => void;
43
+ }
44
+ /**
45
+ * Bootstrap the desktop shell: install the native router and open the window
46
+ * once the app is ready.
47
+ *
48
+ * @param opts Bootstrap options; `app`/`ipcMain`/`BrowserWindowCtor` are
49
+ * injectable for tests.
50
+ */
51
+ declare function startDesktop(opts: StartDesktopOptions): void;
52
+
53
+ export { type AppLike, type StartDesktopOptions, startDesktop };
@@ -0,0 +1,18 @@
1
+ import { createWindow } from "./create-window";
2
+ import { installNativeRouter } from "../bridge/main-router";
3
+ function startDesktop(opts) {
4
+ const onError = opts.onError ?? ((error) => {
5
+ console.error("[sublime/desktop] startDesktop failed:", error);
6
+ });
7
+ void opts.app.whenReady().then(() => {
8
+ installNativeRouter(opts.ipcMain);
9
+ createWindow({
10
+ entry: opts.entry,
11
+ preload: opts.preload,
12
+ ...opts.BrowserWindowCtor !== void 0 ? { BrowserWindowCtor: opts.BrowserWindowCtor } : {}
13
+ });
14
+ }).catch(onError);
15
+ }
16
+ export {
17
+ startDesktop
18
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Core contract types for native services.
3
+ *
4
+ * A native service is a named bag of async methods authored in the main
5
+ * process. Methods are always async because every call crosses the
6
+ * `native:invoke` IPC boundary.
7
+ */
8
+ /** A map of async native methods keyed by method name. */
9
+ type NativeMethods = Record<string, (...args: any[]) => Promise<any>>;
10
+ /** A named native service: its `name` plus its async `methods`. */
11
+ interface NativeService<M extends NativeMethods = NativeMethods> {
12
+ name: string;
13
+ methods: M;
14
+ }
15
+
16
+ export type { NativeMethods, NativeService };
package/dist/types.js ADDED
File without changes
@@ -0,0 +1,24 @@
1
+ import { NativeMethods } from './types.js';
2
+
3
+ /**
4
+ * Renderer hook for consuming a native service from React.
5
+ *
6
+ * `useNative('fs')` reads the `window.sublimeNative` bridge exposed by the
7
+ * preload (see `exposeNativeBridge`). On plain web — where no preload ran and
8
+ * the bridge is absent — it returns `null`, letting components gracefully
9
+ * degrade. Inside the Electron shell it returns a typed {@link createProxy}
10
+ * whose `invoke` forwards over the one `native:invoke` channel and revives any
11
+ * `{ __nativeError }` envelope (produced by the main router) back into a
12
+ * {@link NativeError} so the caller sees a normal rejected promise.
13
+ */
14
+
15
+ /**
16
+ * Access a native service by name from the renderer.
17
+ *
18
+ * @typeParam M the service's method map (from the `defineNative` author).
19
+ * @param name the registry key of the native service (e.g. `'fs'`).
20
+ * @returns a typed proxy, or `null` when running outside the Electron shell.
21
+ */
22
+ declare function useNative<M extends NativeMethods>(name: string): M | null;
23
+
24
+ export { useNative };
@@ -0,0 +1,21 @@
1
+ import { createProxy } from "./bridge/proxy";
2
+ import { deserializeError } from "./errors";
3
+ function isNativeErrorEnvelope(value) {
4
+ return typeof value === "object" && value !== null && "__nativeError" in value;
5
+ }
6
+ function useNative(name) {
7
+ const bridge = globalThis.sublimeNative;
8
+ if (bridge === void 0) {
9
+ return null;
10
+ }
11
+ return createProxy(name, async (mod, method, args) => {
12
+ const result = await bridge.invoke(mod, method, args);
13
+ if (isNativeErrorEnvelope(result)) {
14
+ throw deserializeError(result.__nativeError);
15
+ }
16
+ return result;
17
+ });
18
+ }
19
+ export {
20
+ useNative
21
+ };
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@sublime-ui/desktop",
3
+ "version": "0.1.0",
4
+ "description": "Electron desktop packaging and a secure native bridge (defineNative/useNative over one IPC channel) for Sublime UI apps.",
5
+ "keywords": ["sublime-ui", "electron", "desktop", "ipc", "native-bridge", "electron-forge", "cross-platform", "typescript"],
6
+ "homepage": "https://sublime-ui.github.io/sublime-ui/",
7
+ "bugs": "https://github.com/sublime-ui/sublime-ui/issues",
8
+ "repository": { "type": "git", "url": "git+https://github.com/sublime-ui/sublime-ui.git", "directory": "desktop" },
9
+ "license": "MIT",
10
+ "author": "Aaron Mkandawire",
11
+ "publishConfig": { "access": "public" },
12
+ "type": "module",
13
+ "sideEffects": false,
14
+ "exports": {
15
+ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" },
16
+ "./client": { "types": "./dist/client.d.ts", "import": "./dist/client.js" }
17
+ },
18
+ "files": ["dist", "src"],
19
+ "scripts": {
20
+ "build": "tsup",
21
+ "typecheck": "tsc --noEmit",
22
+ "test": "vitest run --passWithNoTests",
23
+ "lint": "eslint src"
24
+ },
25
+ "peerDependencies": { "electron": ">=30", "react": ">=18" },
26
+ "peerDependenciesMeta": { "electron": { "optional": true }, "react": { "optional": true } },
27
+ "devDependencies": {
28
+ "@types/node": "^22",
29
+ "@types/react": "^18.3.12",
30
+ "electron": "^33.0.0",
31
+ "react": "^18.3.1",
32
+ "@testing-library/react": "^16.0.1",
33
+ "jsdom": "^25.0.1"
34
+ }
35
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Main-process IPC router for the single `native:invoke` channel.
3
+ *
4
+ * Registers one handler that resolves `(module, method)` against the native
5
+ * registry and dispatches. Unknown pairs throw a {@link NativeError}; any
6
+ * throw (unknown or from the impl) is caught and returned as a
7
+ * `{ __nativeError: SerializedError }` envelope, which the renderer proxy
8
+ * (`useNative` / Task 8) revives into a {@link NativeError} and rethrows.
9
+ * Keeping the error convention as a returned envelope — rather than a rejected
10
+ * promise — gives the proxy a single, structured-clone-safe shape to detect.
11
+ */
12
+
13
+ import { resolve } from '../registry';
14
+ import { NativeError, serializeError, type SerializedError } from '../errors';
15
+
16
+ /** Envelope returned over IPC when a native call fails. */
17
+ export interface NativeErrorEnvelope {
18
+ __nativeError: SerializedError;
19
+ }
20
+
21
+ /** Minimal `ipcMain` surface needed to register the channel. Injectable. */
22
+ export interface IpcMainLike {
23
+ handle(channel: string, listener: (e: unknown, ...args: any[]) => any): void;
24
+ }
25
+
26
+ /**
27
+ * Install the `native:invoke` router onto the given `ipcMain`.
28
+ *
29
+ * @param ipcMain Electron's `ipcMain` (or a compatible fake for tests).
30
+ */
31
+ export function installNativeRouter(ipcMain: IpcMainLike): void {
32
+ ipcMain.handle(
33
+ 'native:invoke',
34
+ async (
35
+ _event: unknown,
36
+ mod: string,
37
+ method: string,
38
+ args: unknown[] = [],
39
+ ): Promise<unknown> => {
40
+ try {
41
+ const fn = resolve(mod, method);
42
+ if (fn === undefined) {
43
+ throw new NativeError(`Unknown native method ${mod}:${method}`);
44
+ }
45
+ return await fn(...args);
46
+ } catch (e) {
47
+ const envelope: NativeErrorEnvelope = { __nativeError: serializeError(e) };
48
+ return envelope;
49
+ }
50
+ },
51
+ );
52
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Preload bridge: exposes the typed native surface to the renderer.
3
+ *
4
+ * Runs in Electron's isolated preload context (the only place with access to
5
+ * `contextBridge`). It exposes a single object, `window.sublimeNative`, whose
6
+ * `invoke` forwards every call over the one generic `native:invoke` channel to
7
+ * the main-process router (Task 6). Nothing else is exposed — keeping
8
+ * `contextIsolation: true` / `nodeIntegration: false` meaningful — so the
9
+ * renderer can only reach native functionality through the registry-validated
10
+ * router. `useNative` (Task 8) reads this surface and revives error envelopes.
11
+ */
12
+
13
+ /** Minimal `contextBridge` surface needed to expose the bridge. Injectable. */
14
+ export interface ContextBridgeLike {
15
+ exposeInMainWorld(key: string, api: unknown): void;
16
+ }
17
+
18
+ /** Minimal `ipcRenderer` surface needed to forward invocations. Injectable. */
19
+ export interface IpcRendererLike {
20
+ invoke(channel: string, ...args: any[]): Promise<unknown>;
21
+ }
22
+
23
+ /** Shape exposed at `window.sublimeNative`. */
24
+ export interface SublimeNativeBridge {
25
+ invoke(mod: string, method: string, args: unknown[]): Promise<unknown>;
26
+ }
27
+
28
+ /**
29
+ * Expose the `sublimeNative` bridge on the main world.
30
+ *
31
+ * @param contextBridge Electron's `contextBridge` (or a compatible fake).
32
+ * @param ipcRenderer Electron's `ipcRenderer` (or a compatible fake).
33
+ */
34
+ export function exposeNativeBridge(
35
+ contextBridge: ContextBridgeLike,
36
+ ipcRenderer: IpcRendererLike,
37
+ ): void {
38
+ const bridge: SublimeNativeBridge = {
39
+ invoke: (mod, method, args) =>
40
+ ipcRenderer.invoke('native:invoke', mod, method, args),
41
+ };
42
+ contextBridge.exposeInMainWorld('sublimeNative', bridge);
43
+ }
@@ -0,0 +1,31 @@
1
+ import type { NativeMethods } from '../types';
2
+
3
+ /**
4
+ * Build the renderer-side typed proxy for a native service.
5
+ *
6
+ * Every property access on the returned object yields a function that forwards
7
+ * its call to `invoke(mod, method, args)` — the single `native:invoke` IPC
8
+ * channel. No node dependencies enter the renderer: the proxy only knows the
9
+ * module name and forwards arguments, while the `M` type parameter (derived
10
+ * from the service authored with `defineNative`) gives end-to-end type safety.
11
+ *
12
+ * @param mod the native service name (the registry key).
13
+ * @param invoke the transport: forwards `(mod, method, args)` over IPC.
14
+ *
15
+ * @example
16
+ * const printer = createProxy<{ print: (copies: number) => Promise<string> }>(
17
+ * 'printer',
18
+ * invoke,
19
+ * );
20
+ * await printer.print(7); // invoke('printer', 'print', [7])
21
+ */
22
+ export function createProxy<M extends NativeMethods>(
23
+ mod: string,
24
+ invoke: (mod: string, method: string, args: unknown[]) => Promise<unknown>,
25
+ ): M {
26
+ return new Proxy({} as M, {
27
+ get(_target, prop) {
28
+ return (...args: unknown[]) => invoke(mod, String(prop), args);
29
+ },
30
+ });
31
+ }
package/src/client.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Renderer-safe public API for `@sublime-ui/desktop` (`@sublime-ui/desktop/client`).
3
+ *
4
+ * This barrel re-exports ONLY the modules that are safe to pull into a web
5
+ * renderer bundle: nothing here transitively imports `node:*` or `electron`.
6
+ * An app's renderer can `import { useNative } from '@sublime-ui/desktop/client'`
7
+ * and webpack will never see node/electron code, regardless of tree-shaking.
8
+ *
9
+ * The main barrel (`@sublime-ui/desktop`) additionally re-exports the
10
+ * main-process shell + built-in services, which DO pull in node/electron; it is
11
+ * marked side-effect-free so a `useNative`-only import can still be shaken, but
12
+ * `./client` is the guaranteed-safe entry for renderer code.
13
+ *
14
+ * @remarks Author services in the main process from the main barrel (or its
15
+ * `./services`), and `defineNative` is exported here too so renderer-shared
16
+ * contract types stay reachable without crossing into node/electron.
17
+ */
18
+
19
+ // Authoring + contract types (pure — no node/electron).
20
+ export { defineNative } from './define-native';
21
+ export type { NativeMethods, NativeService } from './types';
22
+
23
+ // Typed error transport (pure).
24
+ export {
25
+ NativeError,
26
+ serializeError,
27
+ deserializeError,
28
+ type SerializedError,
29
+ } from './errors';
30
+
31
+ // Renderer hook + proxy (pure — forward over the single IPC channel only).
32
+ export { useNative } from './use-native';
33
+ export { createProxy } from './bridge/proxy';
@@ -0,0 +1,21 @@
1
+ import type { NativeMethods, NativeService } from './types';
2
+
3
+ /**
4
+ * Author a native service from the main process.
5
+ *
6
+ * A thin, fully-typed wrapper: it pairs a service `name` with its async
7
+ * `methods` so that `typeof service` preserves each method's signature.
8
+ * That preserved type is what the renderer contract is derived from, giving
9
+ * end-to-end type safety across the `native:invoke` IPC boundary.
10
+ *
11
+ * @example
12
+ * const printer = defineNative('printer', {
13
+ * print: async (copies: number): Promise<string> => `printed ${copies}`,
14
+ * });
15
+ */
16
+ export function defineNative<M extends NativeMethods>(
17
+ name: string,
18
+ methods: M,
19
+ ): NativeService<M> {
20
+ return { name, methods };
21
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Typed error transport for the native bridge.
3
+ *
4
+ * Errors thrown by a native service in the main process are serialized to a
5
+ * plain {@link SerializedError} so they can cross the `native:invoke` IPC
6
+ * boundary, then revived into a {@link NativeError} on the renderer side.
7
+ */
8
+
9
+ /** Plain, structured-clone-safe representation of a native error. */
10
+ export interface SerializedError {
11
+ name: string;
12
+ message: string;
13
+ code?: string;
14
+ }
15
+
16
+ /** Error type rethrown on the renderer when a native call fails. */
17
+ export class NativeError extends Error {
18
+ code?: string;
19
+
20
+ constructor(message: string, code?: string) {
21
+ super(message);
22
+ this.name = 'NativeError';
23
+ if (code !== undefined) {
24
+ this.code = code;
25
+ }
26
+ }
27
+ }
28
+
29
+ /** Coerce any throwable into a transport-safe {@link SerializedError}. */
30
+ export function serializeError(e: unknown): SerializedError {
31
+ if (e instanceof Error) {
32
+ const code = (e as { code?: unknown }).code;
33
+ const out: SerializedError = { name: e.name, message: e.message };
34
+ if (typeof code === 'string') {
35
+ out.code = code;
36
+ }
37
+ return out;
38
+ }
39
+ return { name: 'Error', message: String(e) };
40
+ }
41
+
42
+ /** Revive a {@link SerializedError} into a {@link NativeError}. */
43
+ export function deserializeError(s: SerializedError): NativeError {
44
+ return new NativeError(s.message, s.code);
45
+ }
package/src/index.ts ADDED
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Public API for `@sublime-ui/desktop`.
3
+ *
4
+ * A typed native bridge over one secure generic IPC channel (`native:invoke`):
5
+ * author services in the main process with {@link defineNative}, register them
6
+ * with {@link registerNative}, dispatch them through {@link installNativeRouter}
7
+ * / {@link exposeNativeBridge}, and consume them in the renderer with
8
+ * {@link useNative}. {@link startDesktop} / {@link createWindow} provide the
9
+ * hardened Electron shell, and the built-in services (`fs`, `dialog`, `shell`,
10
+ * `clipboard`, `notifications`) cover the common cases out of the box.
11
+ *
12
+ * @remarks
13
+ * The renderer must import native modules type-only; the only runtime crossing
14
+ * is the single `native:invoke` channel.
15
+ */
16
+
17
+ // Core authoring + contract types.
18
+ export { defineNative } from './define-native';
19
+ export { registerNative } from './registry';
20
+ export type { NativeMethods, NativeService } from './types';
21
+
22
+ // Typed error transport.
23
+ export {
24
+ NativeError,
25
+ serializeError,
26
+ deserializeError,
27
+ type SerializedError,
28
+ } from './errors';
29
+
30
+ // Renderer hook + proxy.
31
+ export { useNative } from './use-native';
32
+ export { createProxy } from './bridge/proxy';
33
+
34
+ // Main-process router + preload bridge.
35
+ export {
36
+ installNativeRouter,
37
+ type IpcMainLike,
38
+ type NativeErrorEnvelope,
39
+ } from './bridge/main-router';
40
+ export {
41
+ exposeNativeBridge,
42
+ type ContextBridgeLike,
43
+ type IpcRendererLike,
44
+ type SublimeNativeBridge,
45
+ } from './bridge/preload';
46
+
47
+ // Hardened Electron shell.
48
+ export {
49
+ createWindow,
50
+ type BrowserWindowLike,
51
+ type BrowserWindowCtor,
52
+ type CreateWindowOptions,
53
+ } from './shell/create-window';
54
+ export {
55
+ startDesktop,
56
+ type AppLike,
57
+ type StartDesktopOptions,
58
+ } from './shell/main';
59
+
60
+ // Built-in services.
61
+ export {
62
+ fs,
63
+ dialog,
64
+ shell,
65
+ clipboard,
66
+ notifications,
67
+ type NotifyOptions,
68
+ } from './services/index';
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Native service registry.
3
+ *
4
+ * Services authored in the main process are registered here; the
5
+ * `native:invoke` router resolves `(module, method)` pairs against this
6
+ * registry and dispatches. Anything not registered resolves to `undefined`,
7
+ * which the router treats as an unknown-method rejection.
8
+ */
9
+
10
+ import type { NativeMethods, NativeService } from './types';
11
+
12
+ const services = new Map<string, NativeService>();
13
+
14
+ /** Register one or more native services, keyed by their `name`. */
15
+ export function registerNative(toRegister: NativeService[]): void {
16
+ for (const service of toRegister) {
17
+ services.set(service.name, service);
18
+ }
19
+ }
20
+
21
+ /**
22
+ * Resolve a `(module, method)` pair to its implementation, or `undefined`
23
+ * when the module is unknown or the method is not exposed by it.
24
+ */
25
+ export function resolve(
26
+ mod: string,
27
+ method: string,
28
+ ): ((...args: any[]) => Promise<any>) | undefined {
29
+ const methods: NativeMethods | undefined = services.get(mod)?.methods;
30
+ if (methods === undefined) {
31
+ return undefined;
32
+ }
33
+ // OWN-property lookup only: a renderer must never reach inherited members
34
+ // (`constructor`, `__proto__`, `toString`, `hasOwnProperty`, `valueOf`, …)
35
+ // through the prototype chain, which `methods[method]` would expose.
36
+ return Object.prototype.hasOwnProperty.call(methods, method)
37
+ ? methods[method]
38
+ : undefined;
39
+ }
40
+
41
+ /** Clear all registered services. Test seam. */
42
+ export function clearRegistry(): void {
43
+ services.clear();
44
+ }
@@ -0,0 +1,21 @@
1
+ import { defineNative } from '../define-native';
2
+ import { getElectron } from './get-electron';
3
+
4
+ /**
5
+ * Built-in `clipboard` native service.
6
+ *
7
+ * Thin async wrappers over Electron's synchronous `clipboard` module so the
8
+ * surface stays uniform across the native bridge (every method is a Promise).
9
+ * The Electron module is resolved lazily so the package loads without Electron
10
+ * and unit tests can mock it.
11
+ */
12
+ export const clipboard = defineNative('clipboard', {
13
+ readText: async (): Promise<string> => {
14
+ const { clipboard: c } = await getElectron();
15
+ return c.readText();
16
+ },
17
+ writeText: async (text: string): Promise<void> => {
18
+ const { clipboard: c } = await getElectron();
19
+ c.writeText(text);
20
+ },
21
+ });
@@ -0,0 +1,31 @@
1
+ import type { MessageBoxOptions } from 'electron';
2
+ import { defineNative } from '../define-native';
3
+ import { getElectron } from './get-electron';
4
+
5
+ /**
6
+ * Built-in `dialog` native service.
7
+ *
8
+ * Thin async wrappers over Electron's `dialog` module. File pickers collapse
9
+ * Electron's `{ canceled, filePaths }` / `{ canceled, filePath }` shapes down
10
+ * to a single selected path or `null` when the user cancels (or selects
11
+ * nothing). The Electron module is resolved lazily so the package loads
12
+ * without Electron and unit tests can mock it.
13
+ */
14
+ export const dialog = defineNative('dialog', {
15
+ openFile: async (): Promise<string | null> => {
16
+ const { dialog: d } = await getElectron();
17
+ const result = await d.showOpenDialog({ properties: ['openFile'] });
18
+ if (result.canceled) return null;
19
+ return result.filePaths[0] ?? null;
20
+ },
21
+ saveFile: async (): Promise<string | null> => {
22
+ const { dialog: d } = await getElectron();
23
+ const result = await d.showSaveDialog({});
24
+ if (result.canceled) return null;
25
+ return result.filePath ?? null;
26
+ },
27
+ message: async (opts: MessageBoxOptions): Promise<void> => {
28
+ const { dialog: d } = await getElectron();
29
+ await d.showMessageBox(opts);
30
+ },
31
+ });