@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.
- package/LICENSE +21 -0
- package/README.md +47 -0
- package/dist/bridge/main-router.d.ts +30 -0
- package/dist/bridge/main-router.js +22 -0
- package/dist/bridge/preload.d.ts +32 -0
- package/dist/bridge/preload.js +9 -0
- package/dist/bridge/proxy.d.ts +24 -0
- package/dist/bridge/proxy.js +10 -0
- package/dist/client.d.ts +5 -0
- package/dist/client.js +16 -0
- package/dist/define-native.d.ts +18 -0
- package/dist/define-native.js +6 -0
- package/dist/errors.d.ts +24 -0
- package/dist/errors.js +29 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +46 -0
- package/dist/registry.d.ts +22 -0
- package/dist/registry.js +21 -0
- package/dist/services/clipboard.d.ts +16 -0
- package/dist/services/clipboard.js +15 -0
- package/dist/services/dialog.d.ts +19 -0
- package/dist/services/dialog.js +23 -0
- package/dist/services/fs.d.ts +20 -0
- package/dist/services/fs.js +26 -0
- package/dist/services/get-electron.d.ts +14 -0
- package/dist/services/get-electron.js +6 -0
- package/dist/services/index.d.ts +7 -0
- package/dist/services/index.js +12 -0
- package/dist/services/notifications.d.ts +21 -0
- package/dist/services/notifications.js +11 -0
- package/dist/services/shell.d.ts +16 -0
- package/dist/services/shell.js +19 -0
- package/dist/shell/create-window.d.ts +48 -0
- package/dist/shell/create-window.js +28 -0
- package/dist/shell/main.d.ts +53 -0
- package/dist/shell/main.js +18 -0
- package/dist/types.d.ts +16 -0
- package/dist/types.js +0 -0
- package/dist/use-native.d.ts +24 -0
- package/dist/use-native.js +21 -0
- package/package.json +35 -0
- package/src/bridge/main-router.ts +52 -0
- package/src/bridge/preload.ts +43 -0
- package/src/bridge/proxy.ts +31 -0
- package/src/client.ts +33 -0
- package/src/define-native.ts +21 -0
- package/src/errors.ts +45 -0
- package/src/index.ts +68 -0
- package/src/registry.ts +44 -0
- package/src/services/clipboard.ts +21 -0
- package/src/services/dialog.ts +31 -0
- package/src/services/fs.ts +32 -0
- package/src/services/get-electron.ts +14 -0
- package/src/services/index.ts +13 -0
- package/src/services/notifications.ts +24 -0
- package/src/services/shell.ts +24 -0
- package/src/shell/create-window.ts +75 -0
- package/src/shell/main.ts +70 -0
- package/src/types.ts +16 -0
- package/src/use-native.ts +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aaron Mkandawire and Sublime UI contributors
|
|
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
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# @sublime-ui/desktop
|
|
2
|
+
|
|
3
|
+
Electron desktop packaging and a secure **native bridge** for
|
|
4
|
+
[Sublime UI](https://sublime-ui.github.io/sublime-ui/) apps. The desktop target
|
|
5
|
+
renders your existing **web** UI and adds access to native capabilities.
|
|
6
|
+
|
|
7
|
+
Define a native service in the main process and call it from the renderer with a
|
|
8
|
+
typed hook. Everything travels over one generic, `contextIsolation`-safe IPC
|
|
9
|
+
channel.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
// main process — define + register
|
|
13
|
+
import { defineNative, registerNative } from '@sublime-ui/desktop';
|
|
14
|
+
export const greeter = defineNative('greeter', {
|
|
15
|
+
async hello(name: string) { return `Hello, ${name}!`; },
|
|
16
|
+
});
|
|
17
|
+
registerNative([greeter]);
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// renderer — renderer-safe entry, returns null on web/mobile
|
|
22
|
+
import { useNative } from '@sublime-ui/desktop/client';
|
|
23
|
+
const greeter = useNative<typeof import('./greeter').greeter>('greeter');
|
|
24
|
+
await greeter?.hello('world');
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Built-in services cover `fs`, `dialog`, `shell`, `clipboard`, and
|
|
28
|
+
`notifications`. Packaging uses Electron Forge via
|
|
29
|
+
`sublime desktop:dev` / `sublime desktop:build`.
|
|
30
|
+
|
|
31
|
+
> Import renderer code from `@sublime-ui/desktop/client` — it contains **no**
|
|
32
|
+
> node/electron and is safe to bundle into the web build.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm install @sublime-ui/desktop
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Documentation
|
|
41
|
+
|
|
42
|
+
The native bridge and packaging:
|
|
43
|
+
**https://sublime-ui.github.io/sublime-ui/docs/desktop/overview**
|
|
44
|
+
|
|
45
|
+
## License
|
|
46
|
+
|
|
47
|
+
MIT
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { SerializedError } from '../errors.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Main-process IPC router for the single `native:invoke` channel.
|
|
5
|
+
*
|
|
6
|
+
* Registers one handler that resolves `(module, method)` against the native
|
|
7
|
+
* registry and dispatches. Unknown pairs throw a {@link NativeError}; any
|
|
8
|
+
* throw (unknown or from the impl) is caught and returned as a
|
|
9
|
+
* `{ __nativeError: SerializedError }` envelope, which the renderer proxy
|
|
10
|
+
* (`useNative` / Task 8) revives into a {@link NativeError} and rethrows.
|
|
11
|
+
* Keeping the error convention as a returned envelope — rather than a rejected
|
|
12
|
+
* promise — gives the proxy a single, structured-clone-safe shape to detect.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** Envelope returned over IPC when a native call fails. */
|
|
16
|
+
interface NativeErrorEnvelope {
|
|
17
|
+
__nativeError: SerializedError;
|
|
18
|
+
}
|
|
19
|
+
/** Minimal `ipcMain` surface needed to register the channel. Injectable. */
|
|
20
|
+
interface IpcMainLike {
|
|
21
|
+
handle(channel: string, listener: (e: unknown, ...args: any[]) => any): void;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Install the `native:invoke` router onto the given `ipcMain`.
|
|
25
|
+
*
|
|
26
|
+
* @param ipcMain Electron's `ipcMain` (or a compatible fake for tests).
|
|
27
|
+
*/
|
|
28
|
+
declare function installNativeRouter(ipcMain: IpcMainLike): void;
|
|
29
|
+
|
|
30
|
+
export { type IpcMainLike, type NativeErrorEnvelope, installNativeRouter };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { resolve } from "../registry";
|
|
2
|
+
import { NativeError, serializeError } from "../errors";
|
|
3
|
+
function installNativeRouter(ipcMain) {
|
|
4
|
+
ipcMain.handle(
|
|
5
|
+
"native:invoke",
|
|
6
|
+
async (_event, mod, method, args = []) => {
|
|
7
|
+
try {
|
|
8
|
+
const fn = resolve(mod, method);
|
|
9
|
+
if (fn === void 0) {
|
|
10
|
+
throw new NativeError(`Unknown native method ${mod}:${method}`);
|
|
11
|
+
}
|
|
12
|
+
return await fn(...args);
|
|
13
|
+
} catch (e) {
|
|
14
|
+
const envelope = { __nativeError: serializeError(e) };
|
|
15
|
+
return envelope;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
export {
|
|
21
|
+
installNativeRouter
|
|
22
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
/** Minimal `contextBridge` surface needed to expose the bridge. Injectable. */
|
|
13
|
+
interface ContextBridgeLike {
|
|
14
|
+
exposeInMainWorld(key: string, api: unknown): void;
|
|
15
|
+
}
|
|
16
|
+
/** Minimal `ipcRenderer` surface needed to forward invocations. Injectable. */
|
|
17
|
+
interface IpcRendererLike {
|
|
18
|
+
invoke(channel: string, ...args: any[]): Promise<unknown>;
|
|
19
|
+
}
|
|
20
|
+
/** Shape exposed at `window.sublimeNative`. */
|
|
21
|
+
interface SublimeNativeBridge {
|
|
22
|
+
invoke(mod: string, method: string, args: unknown[]): Promise<unknown>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Expose the `sublimeNative` bridge on the main world.
|
|
26
|
+
*
|
|
27
|
+
* @param contextBridge Electron's `contextBridge` (or a compatible fake).
|
|
28
|
+
* @param ipcRenderer Electron's `ipcRenderer` (or a compatible fake).
|
|
29
|
+
*/
|
|
30
|
+
declare function exposeNativeBridge(contextBridge: ContextBridgeLike, ipcRenderer: IpcRendererLike): void;
|
|
31
|
+
|
|
32
|
+
export { type ContextBridgeLike, type IpcRendererLike, type SublimeNativeBridge, exposeNativeBridge };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
function exposeNativeBridge(contextBridge, ipcRenderer) {
|
|
2
|
+
const bridge = {
|
|
3
|
+
invoke: (mod, method, args) => ipcRenderer.invoke("native:invoke", mod, method, args)
|
|
4
|
+
};
|
|
5
|
+
contextBridge.exposeInMainWorld("sublimeNative", bridge);
|
|
6
|
+
}
|
|
7
|
+
export {
|
|
8
|
+
exposeNativeBridge
|
|
9
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { NativeMethods } from '../types.js';
|
|
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
|
+
declare function createProxy<M extends NativeMethods>(mod: string, invoke: (mod: string, method: string, args: unknown[]) => Promise<unknown>): M;
|
|
23
|
+
|
|
24
|
+
export { createProxy };
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { defineNative } from './define-native.js';
|
|
2
|
+
export { NativeMethods, NativeService } from './types.js';
|
|
3
|
+
export { NativeError, SerializedError, deserializeError, serializeError } from './errors.js';
|
|
4
|
+
export { useNative } from './use-native.js';
|
|
5
|
+
export { createProxy } from './bridge/proxy.js';
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineNative } from "./define-native";
|
|
2
|
+
import {
|
|
3
|
+
NativeError,
|
|
4
|
+
serializeError,
|
|
5
|
+
deserializeError
|
|
6
|
+
} from "./errors";
|
|
7
|
+
import { useNative } from "./use-native";
|
|
8
|
+
import { createProxy } from "./bridge/proxy";
|
|
9
|
+
export {
|
|
10
|
+
NativeError,
|
|
11
|
+
createProxy,
|
|
12
|
+
defineNative,
|
|
13
|
+
deserializeError,
|
|
14
|
+
serializeError,
|
|
15
|
+
useNative
|
|
16
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { NativeMethods, NativeService } from './types.js';
|
|
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
|
+
declare function defineNative<M extends NativeMethods>(name: string, methods: M): NativeService<M>;
|
|
17
|
+
|
|
18
|
+
export { defineNative };
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
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
|
+
/** Plain, structured-clone-safe representation of a native error. */
|
|
9
|
+
interface SerializedError {
|
|
10
|
+
name: string;
|
|
11
|
+
message: string;
|
|
12
|
+
code?: string;
|
|
13
|
+
}
|
|
14
|
+
/** Error type rethrown on the renderer when a native call fails. */
|
|
15
|
+
declare class NativeError extends Error {
|
|
16
|
+
code?: string;
|
|
17
|
+
constructor(message: string, code?: string);
|
|
18
|
+
}
|
|
19
|
+
/** Coerce any throwable into a transport-safe {@link SerializedError}. */
|
|
20
|
+
declare function serializeError(e: unknown): SerializedError;
|
|
21
|
+
/** Revive a {@link SerializedError} into a {@link NativeError}. */
|
|
22
|
+
declare function deserializeError(s: SerializedError): NativeError;
|
|
23
|
+
|
|
24
|
+
export { NativeError, type SerializedError, deserializeError, serializeError };
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
class NativeError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(message, code) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "NativeError";
|
|
6
|
+
if (code !== void 0) {
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function serializeError(e) {
|
|
12
|
+
if (e instanceof Error) {
|
|
13
|
+
const code = e.code;
|
|
14
|
+
const out = { name: e.name, message: e.message };
|
|
15
|
+
if (typeof code === "string") {
|
|
16
|
+
out.code = code;
|
|
17
|
+
}
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
return { name: "Error", message: String(e) };
|
|
21
|
+
}
|
|
22
|
+
function deserializeError(s) {
|
|
23
|
+
return new NativeError(s.message, s.code);
|
|
24
|
+
}
|
|
25
|
+
export {
|
|
26
|
+
NativeError,
|
|
27
|
+
deserializeError,
|
|
28
|
+
serializeError
|
|
29
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export { defineNative } from './define-native.js';
|
|
2
|
+
export { registerNative } from './registry.js';
|
|
3
|
+
export { NativeMethods, NativeService } from './types.js';
|
|
4
|
+
export { NativeError, SerializedError, deserializeError, serializeError } from './errors.js';
|
|
5
|
+
export { useNative } from './use-native.js';
|
|
6
|
+
export { createProxy } from './bridge/proxy.js';
|
|
7
|
+
export { IpcMainLike, NativeErrorEnvelope, installNativeRouter } from './bridge/main-router.js';
|
|
8
|
+
export { ContextBridgeLike, IpcRendererLike, SublimeNativeBridge, exposeNativeBridge } from './bridge/preload.js';
|
|
9
|
+
export { BrowserWindowCtor, BrowserWindowLike, CreateWindowOptions, createWindow } from './shell/create-window.js';
|
|
10
|
+
export { AppLike, StartDesktopOptions, startDesktop } from './shell/main.js';
|
|
11
|
+
export { fs } from './services/fs.js';
|
|
12
|
+
export { dialog } from './services/dialog.js';
|
|
13
|
+
export { shell } from './services/shell.js';
|
|
14
|
+
export { clipboard } from './services/clipboard.js';
|
|
15
|
+
export { NotifyOptions, notifications } from './services/notifications.js';
|
|
16
|
+
import 'electron';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { defineNative } from "./define-native";
|
|
2
|
+
import { registerNative } from "./registry";
|
|
3
|
+
import {
|
|
4
|
+
NativeError,
|
|
5
|
+
serializeError,
|
|
6
|
+
deserializeError
|
|
7
|
+
} from "./errors";
|
|
8
|
+
import { useNative } from "./use-native";
|
|
9
|
+
import { createProxy } from "./bridge/proxy";
|
|
10
|
+
import {
|
|
11
|
+
installNativeRouter
|
|
12
|
+
} from "./bridge/main-router";
|
|
13
|
+
import {
|
|
14
|
+
exposeNativeBridge
|
|
15
|
+
} from "./bridge/preload";
|
|
16
|
+
import {
|
|
17
|
+
createWindow
|
|
18
|
+
} from "./shell/create-window";
|
|
19
|
+
import {
|
|
20
|
+
startDesktop
|
|
21
|
+
} from "./shell/main";
|
|
22
|
+
import {
|
|
23
|
+
fs,
|
|
24
|
+
dialog,
|
|
25
|
+
shell,
|
|
26
|
+
clipboard,
|
|
27
|
+
notifications
|
|
28
|
+
} from "./services/index";
|
|
29
|
+
export {
|
|
30
|
+
NativeError,
|
|
31
|
+
clipboard,
|
|
32
|
+
createProxy,
|
|
33
|
+
createWindow,
|
|
34
|
+
defineNative,
|
|
35
|
+
deserializeError,
|
|
36
|
+
dialog,
|
|
37
|
+
exposeNativeBridge,
|
|
38
|
+
fs,
|
|
39
|
+
installNativeRouter,
|
|
40
|
+
notifications,
|
|
41
|
+
registerNative,
|
|
42
|
+
serializeError,
|
|
43
|
+
shell,
|
|
44
|
+
startDesktop,
|
|
45
|
+
useNative
|
|
46
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { NativeService } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Native service registry.
|
|
5
|
+
*
|
|
6
|
+
* Services authored in the main process are registered here; the
|
|
7
|
+
* `native:invoke` router resolves `(module, method)` pairs against this
|
|
8
|
+
* registry and dispatches. Anything not registered resolves to `undefined`,
|
|
9
|
+
* which the router treats as an unknown-method rejection.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Register one or more native services, keyed by their `name`. */
|
|
13
|
+
declare function registerNative(toRegister: NativeService[]): void;
|
|
14
|
+
/**
|
|
15
|
+
* Resolve a `(module, method)` pair to its implementation, or `undefined`
|
|
16
|
+
* when the module is unknown or the method is not exposed by it.
|
|
17
|
+
*/
|
|
18
|
+
declare function resolve(mod: string, method: string): ((...args: any[]) => Promise<any>) | undefined;
|
|
19
|
+
/** Clear all registered services. Test seam. */
|
|
20
|
+
declare function clearRegistry(): void;
|
|
21
|
+
|
|
22
|
+
export { clearRegistry, registerNative, resolve };
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
const services = /* @__PURE__ */ new Map();
|
|
2
|
+
function registerNative(toRegister) {
|
|
3
|
+
for (const service of toRegister) {
|
|
4
|
+
services.set(service.name, service);
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
function resolve(mod, method) {
|
|
8
|
+
const methods = services.get(mod)?.methods;
|
|
9
|
+
if (methods === void 0) {
|
|
10
|
+
return void 0;
|
|
11
|
+
}
|
|
12
|
+
return Object.prototype.hasOwnProperty.call(methods, method) ? methods[method] : void 0;
|
|
13
|
+
}
|
|
14
|
+
function clearRegistry() {
|
|
15
|
+
services.clear();
|
|
16
|
+
}
|
|
17
|
+
export {
|
|
18
|
+
clearRegistry,
|
|
19
|
+
registerNative,
|
|
20
|
+
resolve
|
|
21
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NativeService } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Built-in `clipboard` native service.
|
|
5
|
+
*
|
|
6
|
+
* Thin async wrappers over Electron's synchronous `clipboard` module so the
|
|
7
|
+
* surface stays uniform across the native bridge (every method is a Promise).
|
|
8
|
+
* The Electron module is resolved lazily so the package loads without Electron
|
|
9
|
+
* and unit tests can mock it.
|
|
10
|
+
*/
|
|
11
|
+
declare const clipboard: NativeService<{
|
|
12
|
+
readText: () => Promise<string>;
|
|
13
|
+
writeText: (text: string) => Promise<void>;
|
|
14
|
+
}>;
|
|
15
|
+
|
|
16
|
+
export { clipboard };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { defineNative } from "../define-native";
|
|
2
|
+
import { getElectron } from "./get-electron";
|
|
3
|
+
const clipboard = defineNative("clipboard", {
|
|
4
|
+
readText: async () => {
|
|
5
|
+
const { clipboard: c } = await getElectron();
|
|
6
|
+
return c.readText();
|
|
7
|
+
},
|
|
8
|
+
writeText: async (text) => {
|
|
9
|
+
const { clipboard: c } = await getElectron();
|
|
10
|
+
c.writeText(text);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
export {
|
|
14
|
+
clipboard
|
|
15
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NativeService } from '../types.js';
|
|
2
|
+
import { MessageBoxOptions } from 'electron';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Built-in `dialog` native service.
|
|
6
|
+
*
|
|
7
|
+
* Thin async wrappers over Electron's `dialog` module. File pickers collapse
|
|
8
|
+
* Electron's `{ canceled, filePaths }` / `{ canceled, filePath }` shapes down
|
|
9
|
+
* to a single selected path or `null` when the user cancels (or selects
|
|
10
|
+
* nothing). The Electron module is resolved lazily so the package loads
|
|
11
|
+
* without Electron and unit tests can mock it.
|
|
12
|
+
*/
|
|
13
|
+
declare const dialog: NativeService<{
|
|
14
|
+
openFile: () => Promise<string | null>;
|
|
15
|
+
saveFile: () => Promise<string | null>;
|
|
16
|
+
message: (opts: MessageBoxOptions) => Promise<void>;
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
export { dialog };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineNative } from "../define-native";
|
|
2
|
+
import { getElectron } from "./get-electron";
|
|
3
|
+
const dialog = defineNative("dialog", {
|
|
4
|
+
openFile: async () => {
|
|
5
|
+
const { dialog: d } = await getElectron();
|
|
6
|
+
const result = await d.showOpenDialog({ properties: ["openFile"] });
|
|
7
|
+
if (result.canceled) return null;
|
|
8
|
+
return result.filePaths[0] ?? null;
|
|
9
|
+
},
|
|
10
|
+
saveFile: async () => {
|
|
11
|
+
const { dialog: d } = await getElectron();
|
|
12
|
+
const result = await d.showSaveDialog({});
|
|
13
|
+
if (result.canceled) return null;
|
|
14
|
+
return result.filePath ?? null;
|
|
15
|
+
},
|
|
16
|
+
message: async (opts) => {
|
|
17
|
+
const { dialog: d } = await getElectron();
|
|
18
|
+
await d.showMessageBox(opts);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
export {
|
|
22
|
+
dialog
|
|
23
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NativeService } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Built-in `fs` native service.
|
|
5
|
+
*
|
|
6
|
+
* A thin, async wrapper over `node:fs/promises` exposed through the native
|
|
7
|
+
* bridge. Text files are read/written as UTF-8 strings; `mkdir` is recursive
|
|
8
|
+
* and `remove` maps to `rm` with `recursive` + `force` so it never throws on
|
|
9
|
+
* a missing path.
|
|
10
|
+
*/
|
|
11
|
+
declare const fs: NativeService<{
|
|
12
|
+
readFile: (path: string) => Promise<string>;
|
|
13
|
+
writeFile: (path: string, data: string) => Promise<void>;
|
|
14
|
+
exists: (path: string) => Promise<boolean>;
|
|
15
|
+
readDir: (path: string) => Promise<string[]>;
|
|
16
|
+
mkdir: (path: string) => Promise<void>;
|
|
17
|
+
remove: (path: string) => Promise<void>;
|
|
18
|
+
}>;
|
|
19
|
+
|
|
20
|
+
export { fs };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFile, writeFile, readdir, mkdir as mkdirFs, rm, access } from "node:fs/promises";
|
|
2
|
+
import { defineNative } from "../define-native";
|
|
3
|
+
const fs = defineNative("fs", {
|
|
4
|
+
readFile: (path) => readFile(path, "utf8"),
|
|
5
|
+
writeFile: async (path, data) => {
|
|
6
|
+
await writeFile(path, data, "utf8");
|
|
7
|
+
},
|
|
8
|
+
exists: async (path) => {
|
|
9
|
+
try {
|
|
10
|
+
await access(path);
|
|
11
|
+
return true;
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
readDir: (path) => readdir(path),
|
|
17
|
+
mkdir: async (path) => {
|
|
18
|
+
await mkdirFs(path, { recursive: true });
|
|
19
|
+
},
|
|
20
|
+
remove: async (path) => {
|
|
21
|
+
await rm(path, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
export {
|
|
25
|
+
fs
|
|
26
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as Electron from 'electron';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Lazy, mockable accessor for the `electron` runtime module.
|
|
5
|
+
*
|
|
6
|
+
* The built-in native services run in the main process, where importing
|
|
7
|
+
* `electron` is permitted. Going through this single indirection keeps the
|
|
8
|
+
* import out of module-evaluation time (so the package can be loaded in
|
|
9
|
+
* environments without Electron) and gives unit tests one seam to mock via
|
|
10
|
+
* `vi.mock('electron', …)`.
|
|
11
|
+
*/
|
|
12
|
+
declare function getElectron(): Promise<typeof Electron>;
|
|
13
|
+
|
|
14
|
+
export { getElectron };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { fs } from "./fs";
|
|
2
|
+
import { dialog } from "./dialog";
|
|
3
|
+
import { shell } from "./shell";
|
|
4
|
+
import { clipboard } from "./clipboard";
|
|
5
|
+
import { notifications } from "./notifications";
|
|
6
|
+
export {
|
|
7
|
+
clipboard,
|
|
8
|
+
dialog,
|
|
9
|
+
fs,
|
|
10
|
+
notifications,
|
|
11
|
+
shell
|
|
12
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { NativeService } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/** Options for {@link notifications}'s `notify` method. */
|
|
4
|
+
interface NotifyOptions {
|
|
5
|
+
/** Title line of the notification. */
|
|
6
|
+
title: string;
|
|
7
|
+
/** Body text of the notification. */
|
|
8
|
+
body: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Built-in `notifications` native service.
|
|
12
|
+
*
|
|
13
|
+
* Constructs and shows a native OS notification via Electron's `Notification`
|
|
14
|
+
* class. The Electron module is resolved lazily so the package loads without
|
|
15
|
+
* Electron and unit tests can mock it.
|
|
16
|
+
*/
|
|
17
|
+
declare const notifications: NativeService<{
|
|
18
|
+
notify: ({ title, body }: NotifyOptions) => Promise<void>;
|
|
19
|
+
}>;
|
|
20
|
+
|
|
21
|
+
export { type NotifyOptions, notifications };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineNative } from "../define-native";
|
|
2
|
+
import { getElectron } from "./get-electron";
|
|
3
|
+
const notifications = defineNative("notifications", {
|
|
4
|
+
notify: async ({ title, body }) => {
|
|
5
|
+
const { Notification } = await getElectron();
|
|
6
|
+
new Notification({ title, body }).show();
|
|
7
|
+
}
|
|
8
|
+
});
|
|
9
|
+
export {
|
|
10
|
+
notifications
|
|
11
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NativeService } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Built-in `shell` native service.
|
|
5
|
+
*
|
|
6
|
+
* Thin async wrappers over Electron's `shell` module for opening URLs, paths,
|
|
7
|
+
* and revealing items in the OS file manager. The Electron module is resolved
|
|
8
|
+
* lazily so the package loads without Electron and unit tests can mock it.
|
|
9
|
+
*/
|
|
10
|
+
declare const shell: NativeService<{
|
|
11
|
+
openExternal: (url: string) => Promise<void>;
|
|
12
|
+
openPath: (path: string) => Promise<void>;
|
|
13
|
+
showItemInFolder: (path: string) => Promise<void>;
|
|
14
|
+
}>;
|
|
15
|
+
|
|
16
|
+
export { shell };
|