@multiplatform.one/desktop 6.7.0 → 7.0.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/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # @multiplatform.one/desktop
2
2
 
3
- Desktop platform APIs for multiplatform.one [Tauri](https://tauri.app) apps
4
- file system, dialogs, paths, and updater behind one interface that no-ops (or
5
- throws clearly) on non-desktop platforms.
3
+ Capability interfaces for desktop-only concerns file system, dialogs, paths,
4
+ and updater behind one import that no-ops (or throws clearly) everywhere the
5
+ capability does not exist.
6
6
 
7
7
  ## Install
8
8
 
@@ -10,8 +10,9 @@ throws clearly) on non-desktop platforms.
10
10
  pnpm add @multiplatform.one/desktop
11
11
  ```
12
12
 
13
- Peers (desktop builds only): `@tauri-apps/api`, `@tauri-apps/plugin-dialog`,
14
- `@tauri-apps/plugin-fs`, `@tauri-apps/plugin-updater`.
13
+ Peers (Tauri builds only, all optional): `@tauri-apps/api`,
14
+ `@tauri-apps/plugin-dialog`, `@tauri-apps/plugin-fs`,
15
+ `@tauri-apps/plugin-updater`.
15
16
 
16
17
  ## What it owns
17
18
 
@@ -21,23 +22,40 @@ Peers (desktop builds only): `@tauri-apps/api`, `@tauri-apps/plugin-dialog`,
21
22
  - **`paths`** — well-known directories (home, app data, …)
22
23
  - **`updater`** — check/install app updates
23
24
 
24
- Each API has a web default and a `.tauri` implementation selected at build
25
- time, so shared code can import unconditionally and gate at runtime with
26
- `isTauri` from `@multiplatform.one/platform`.
25
+ Each API is an interface plus a default implementation, so shared code (for
26
+ example `@multiplatform.one/frappe-ui`'s `FrappeForm` delete confirm) can
27
+ import it unconditionally on every platform.
28
+
29
+ > **State of play.** Tauri is the one backend with a full implementation:
30
+ > each API has a `.tauri` file (selected at build time in Tauri bundles) that
31
+ > rides the matching `@tauri-apps/plugin-*` peer. A Tauri window running the
32
+ > plain web bundle (dev mode) gets the web defaults, which work there — the
33
+ > webview has the browser modals.
34
+ >
35
+ > Outside Tauri: `dialog.message`, `.ask`, `.confirm` and `.prompt` are the
36
+ > browser's modal globals and are therefore **web-only**. On GNOME they
37
+ > **throw** a named error: GJS has no `window.confirm`, and neither silently
38
+ > returning `false` (a confirm button that appears broken) nor silently
39
+ > returning `true` (a delete nobody agreed to) is a defensible default for a
40
+ > confirmation. `dialog.open` and `.save` return `null` on both. `fs` and
41
+ > `paths` report unsupported outside Tauri; `updater` is a no-op there.
42
+ >
43
+ > The GNOME target has real GTK equivalents available (`Gtk.FileDialog`,
44
+ > `Adw.AlertDialog`, `GLib.get_user_*_dir`); wiring them up as
45
+ > `index.gnome.ts` files behind these same interfaces is the natural next
46
+ > step — the interfaces exist precisely so that lands as an implementation
47
+ > rather than an API change.
27
48
 
28
49
  ## What it must not do
29
50
 
30
- - No UI and no Tauri window management — this package wraps capability
31
- plugins, not chrome
51
+ - No UI and no window management — this package wraps capabilities, not chrome
32
52
 
33
53
  ## Usage
34
54
 
35
55
  ```ts
36
56
  import { dialog, fs } from "@multiplatform.one/desktop";
37
- import { isTauri } from "@multiplatform.one/platform";
38
57
 
39
58
  export async function exportNotes(contents: string) {
40
- if (!isTauri) return; // web/native fall back elsewhere
41
59
  const path = await dialog.save({ defaultPath: "notes.md" });
42
60
  if (!path) return;
43
61
  await fs.writeTextFile(path, contents);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplatform.one/desktop",
3
- "version": "6.7.0",
3
+ "version": "7.0.0",
4
4
  "description": "Desktop platform APIs (Tauri) for multiplatform.one",
5
5
  "keywords": [
6
6
  "desktop",
@@ -61,6 +61,7 @@
61
61
  },
62
62
  "scripts": {
63
63
  "typecheck": "tsgo --noEmit",
64
- "build": "rm -rf types 2>/dev/null && tsc -p tsconfig.build.json"
64
+ "build": "rm -rf types 2>/dev/null && tsc -p tsconfig.build.json",
65
+ "test": "vitest run --coverage --coverage.provider=v8 --coverage.reporter=text --coverage.reporter=lcov --coverage.reporter=html"
65
66
  }
66
67
  }
@@ -91,3 +91,47 @@ describe("dialog web stubs", () => {
91
91
  });
92
92
  });
93
93
  });
94
+
95
+ /**
96
+ * GJS's `window` is a react-gnome polyfill stub with none of the modal
97
+ * globals on it. Reproduce that shape rather than deleting `window`, because
98
+ * `typeof window !== "undefined"` is true on GNOME and a guard written only
99
+ * against a missing `window` would sail straight past it.
100
+ */
101
+ describe("dialog defaults on a platform with no modal globals (GJS)", () => {
102
+ beforeEach(() => {
103
+ for (const name of ["alert", "confirm", "prompt"] as const) {
104
+ vi.stubGlobal(name, undefined);
105
+ Reflect.deleteProperty(window, name);
106
+ }
107
+ });
108
+
109
+ afterEach(() => {
110
+ vi.unstubAllGlobals();
111
+ });
112
+
113
+ it("still has a window, exactly as GNOME does", () => {
114
+ expect(typeof window).not.toBe("undefined");
115
+ });
116
+
117
+ it.each([
118
+ ["message", () => dialog.message("hi")],
119
+ ["ask", () => dialog.ask("sure?")],
120
+ ["confirm", () => dialog.confirm("delete?")],
121
+ ["prompt", () => dialog.prompt("name?")],
122
+ ])("dialog.%s rejects instead of throwing a bare TypeError", async (api, call) => {
123
+ await expect(call()).rejects.toThrow(
124
+ new RegExp(`dialog\\.${api} needs window\\.(alert|confirm|prompt)`),
125
+ );
126
+ });
127
+
128
+ it("names the package and points at the GNOME backend that would fix it", async () => {
129
+ await expect(dialog.confirm("delete?")).rejects.toThrow(/@multiplatform.one\/desktop/);
130
+ await expect(dialog.confirm("delete?")).rejects.toThrow(/index\.gnome\.ts/);
131
+ });
132
+
133
+ it("leaves open and save working, since their default is an honest null", async () => {
134
+ expect(await dialog.open()).toBeNull();
135
+ expect(await dialog.save()).toBeNull();
136
+ });
137
+ });
@@ -39,26 +39,65 @@ export interface DialogApi {
39
39
  prompt(message: string, defaultValue?: string): Promise<string | null>;
40
40
  }
41
41
 
42
- // Default web implementation using browser APIs
42
+ /**
43
+ * The default `message`/`ask`/`confirm`/`prompt` are the browser's modal
44
+ * globals, so they are **web-only** — not the "safe default every platform
45
+ * gets" the docs used to claim.
46
+ *
47
+ * GJS is the case that matters: react-gnome's polyfills define a stub
48
+ * `window` with no `alert`, `confirm` or `prompt` on it, so these used to
49
+ * dereference `undefined` and throw a bare `TypeError` from inside a shared
50
+ * package, at whatever unrelated call site happened to trigger a confirm.
51
+ * Failing with a named error that says which platform is missing which
52
+ * capability is strictly better than that, and better than the other
53
+ * tempting option — quietly returning `false` — because a delete confirm
54
+ * that silently answers "no" looks like a broken button, while one that
55
+ * silently answers "yes" deletes something nobody confirmed.
56
+ *
57
+ * The real fix is `dialog/index.gnome.ts` on `Adw.AlertDialog`, resolved by
58
+ * the `.gnome.*` extensions the GNOME vite config registers. Until then this
59
+ * throws, loudly, on GNOME.
60
+ */
61
+ function browserDialog<Name extends "alert" | "confirm" | "prompt">(
62
+ name: Name,
63
+ api: string,
64
+ ): NonNullable<Window[Name]> {
65
+ const fn = typeof window === "undefined" ? undefined : window[name];
66
+ if (typeof fn !== "function") {
67
+ throw new Error(
68
+ `@multiplatform.one/desktop: dialog.${api} needs window.${name}, which this platform does not have. ` +
69
+ `The dialog defaults are the browser's modal globals and are web-only; GNOME needs an Adw.AlertDialog ` +
70
+ `backend in dialog/index.gnome.ts.`,
71
+ );
72
+ }
73
+ return fn.bind(window) as NonNullable<Window[Name]>;
74
+ }
75
+
43
76
  export const dialog: DialogApi = {
44
77
  async open(_options?: OpenDialogOptions): Promise<string | string[] | null> {
45
- // Browser file input could be used here, but returning null for simplicity
78
+ // No browser equivalent that returns a path; an <input type="file"> hands
79
+ // back a File, not a filename the fs API could then open.
46
80
  return null;
47
81
  },
48
82
  async save(_options?: SaveDialogOptions): Promise<string | null> {
49
- // Browser download could be used here, but returning null for simplicity
50
83
  return null;
51
84
  },
52
85
  async message(message: string, options?: MessageDialogOptions): Promise<void> {
53
- window.alert(options?.title ? `${options.title}\n\n${message}` : message);
86
+ browserDialog("alert", "message")(options?.title ? `${options.title}\n\n${message}` : message);
54
87
  },
55
88
  async ask(message: string, options?: ConfirmDialogOptions): Promise<boolean> {
56
- return window.confirm(options?.title ? `${options.title}\n\n${message}` : message);
89
+ return browserDialog(
90
+ "confirm",
91
+ "ask",
92
+ )(options?.title ? `${options.title}\n\n${message}` : message);
57
93
  },
58
94
  async confirm(message: string, options?: ConfirmDialogOptions): Promise<boolean> {
59
- return window.confirm(options?.title ? `${options.title}\n\n${message}` : message);
95
+ return browserDialog(
96
+ "confirm",
97
+ "confirm",
98
+ )(options?.title ? `${options.title}\n\n${message}` : message);
60
99
  },
61
100
  async prompt(message: string, defaultValue = ""): Promise<string | null> {
62
- return window.prompt(message, defaultValue);
101
+ return browserDialog("prompt", "prompt")(message, defaultValue);
63
102
  },
64
103
  };
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Auto-updater API for desktop applications
3
3
  *
4
- * This module provides a platform-agnostic interface for checking and installing
5
- * application updates. The actual implementation differs by platform:
6
- * - Tauri: Uses tauri-plugin-updater
7
- * - Web/Mobile: No-op (updates handled by app stores or service workers)
4
+ * A platform-agnostic interface for checking and installing application
5
+ * updates. The Tauri target implements it via tauri-plugin-updater (the
6
+ * `.tauri` file next door). Everywhere else `isSupported()` returning false
7
+ * is the honest answer web and mobile get updates from the service worker
8
+ * or the app store, and the GNOME target is installed by the system package
9
+ * manager.
8
10
  */
9
11
 
10
12
  export interface UpdateInfo {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/dialog/index.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC;IACrE,IAAI,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1D,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvE,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3E,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACxE;AAGD,eAAO,MAAM,MAAM,EAAE,SAqBpB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/dialog/index.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;IACvB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,UAAU,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;IACpC,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,CAAC,CAAC;IACrE,IAAI,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1D,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACxE,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACvE,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,oBAAoB,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3E,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACxE;AAoCD,eAAO,MAAM,MAAM,EAAE,SA2BpB,CAAC"}
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Auto-updater API for desktop applications
3
3
  *
4
- * This module provides a platform-agnostic interface for checking and installing
5
- * application updates. The actual implementation differs by platform:
6
- * - Tauri: Uses tauri-plugin-updater
7
- * - Web/Mobile: No-op (updates handled by app stores or service workers)
4
+ * A platform-agnostic interface for checking and installing application
5
+ * updates. The Tauri target implements it via tauri-plugin-updater (the
6
+ * `.tauri` file next door). Everywhere else `isSupported()` returning false
7
+ * is the honest answer web and mobile get updates from the service worker
8
+ * or the app store, and the GNOME target is installed by the system package
9
+ * manager.
8
10
  */
9
11
  export interface UpdateInfo {
10
12
  version: string;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/updater/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,eAAe,IAAI,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAE9C;;;;OAIG;IACH,kBAAkB,CAAC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE9E;;OAEG;IACH,WAAW,IAAI,OAAO,CAAC;CACxB;AAGD,eAAO,MAAM,OAAO,EAAE,UAYrB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/updater/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,eAAe,IAAI,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IAE9C;;;;OAIG;IACH,kBAAkB,CAAC,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAE9E;;OAEG;IACH,WAAW,IAAI,OAAO,CAAC;CACxB;AAGD,eAAO,MAAM,OAAO,EAAE,UAYrB,CAAC"}