@adecore/shell 0.40.1 → 0.40.2

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
@@ -23,8 +23,8 @@ The package registers no IPC handler and listens to no app-wide Electron event.
23
23
 
24
24
  | Import | What it holds |
25
25
  |---|---|
26
- | `@adecore/shell` | For the main process: `createWindows`, `createWindowState`, `createUpdater`, `createTheme`, `menuTemplateOf`, `staticMenuTemplate`, `createMenuCommands`, `createPageKeys`, `createAppScheme` and the web guards |
27
- | `@adecore/shell/bridge` | The shapes that cross IPC: `MenuSpec`, `MenuNode`, `MENU_ROLES`, `MENU_BAR_ROLES`, `UpdateState`, `ThemeState` and `compareVersions`. It imports nothing from Electron, so a preload and the page read it too. |
26
+ | `@adecore/shell` | For the main process: `createWindows`, `createWindowState`, `createUpdater`, `createTheme`, `menuTemplateOf`, `staticMenuTemplate`, `createMenuCommands`, `createPageKeys`, `createAppScheme`, `createGestureGate` and the web guards |
27
+ | `@adecore/shell/bridge` | The shapes that cross IPC: `MenuSpec`, `MenuNode`, `MENU_ROLES`, `MENU_BAR_ROLES`, `UpdateState`, `ThemeState` and `compareVersions`, and `ipcSubscriber` for a preload. It imports nothing from Electron, so a preload and the page read it too. |
28
28
 
29
29
  ## Documentation
30
30
 
@@ -1,3 +1,4 @@
1
+ export { ipcSubscriber, type IpcListeners, type Subscribe } from './ipc.ts';
1
2
  export { MENU_ROLES, type MenuBarRole, type MenuNode, type MenuRole, type MenuSpec } from './menu.ts';
2
3
  export type { ThemeState } from './theme.ts';
3
4
  export type { UpdateErrorCode, UpdateState } from './update.ts';
@@ -2,5 +2,6 @@
2
2
  * The shapes that cross IPC between the shell and the page it hosts. Nothing here imports Electron, so
3
3
  * a preload and a page read them as well as the main process.
4
4
  */
5
+ export { ipcSubscriber } from './ipc.js';
5
6
  export { MENU_ROLES } from './menu.js';
6
7
  export { compareVersions, isVersion } from './versions.js';
@@ -0,0 +1,6 @@
1
+ export interface IpcListeners {
2
+ on(channel: string, listener: (event: unknown, ...args: unknown[]) => void): unknown;
3
+ removeListener(channel: string, listener: (event: unknown, ...args: unknown[]) => void): unknown;
4
+ }
5
+ export type Subscribe<Args extends unknown[]> = (listener: (...args: Args) => void) => () => void;
6
+ export declare const ipcSubscriber: (ipc: IpcListeners) => <Args extends unknown[]>(channel: string) => Subscribe<Args>;
@@ -0,0 +1,12 @@
1
+ /*
2
+ * For a preload: turns a channel the main process sends on into what the page subscribes with, a
3
+ * listener in and what removes it again out. The event stays behind: it carries the sender, which the
4
+ * page must never hold.
5
+ */
6
+ export const ipcSubscriber = (ipc) => (channel) => (listener) => {
7
+ const handler = (_event, ...args) => listener(...args);
8
+ ipc.on(channel, handler);
9
+ return () => {
10
+ ipc.removeListener(channel, handler);
11
+ };
12
+ };
package/dist/index.d.ts CHANGED
@@ -6,8 +6,8 @@ export { createPageKeys } from './page-keys.ts';
6
6
  export type { PageKey, PageKeys } from './page-keys.ts';
7
7
  export { createUpdater } from './updater.ts';
8
8
  export type { Updater, UpdaterOptions } from './updater.ts';
9
- export { appWindowNavigation, isAppSender, isAppUrl, isExternalLink, isWebLink, originOf } from './web-guards.ts';
10
- export type { NavigationVerdict, SenderFrame } from './web-guards.ts';
9
+ export { appWindowNavigation, createGestureGate, isAppSender, isAppUrl, isExternalLink, isWebLink, originOf } from './web-guards.ts';
10
+ export type { GestureGate, NavigationVerdict, SenderFrame } from './web-guards.ts';
11
11
  export { createWindowState, fileStorage } from './window-state.ts';
12
12
  export type { SavedWindow, StateWindow, WindowDisplay, WindowSize, WindowState, WindowStateOptions, WindowStateStorage } from './window-state.ts';
13
13
  export { createTheme } from './theme.ts';
package/dist/index.js CHANGED
@@ -2,7 +2,7 @@ export { createAppScheme, segmentsOf } from './app-scheme.js';
2
2
  export { createMenuCommands, devToolsAccelerator, menuTemplateOf, staticMenuTemplate } from './menu.js';
3
3
  export { createPageKeys } from './page-keys.js';
4
4
  export { createUpdater } from './updater.js';
5
- export { appWindowNavigation, isAppSender, isAppUrl, isExternalLink, isWebLink, originOf } from './web-guards.js';
5
+ export { appWindowNavigation, createGestureGate, isAppSender, isAppUrl, isExternalLink, isWebLink, originOf } from './web-guards.js';
6
6
  export { createWindowState, fileStorage } from './window-state.js';
7
7
  export { createTheme } from './theme.js';
8
8
  export { createWindows } from './windows.js';
@@ -4,6 +4,11 @@ export declare const isWebLink: (url: string) => boolean;
4
4
  export declare const isExternalLink: (url: string) => boolean;
5
5
  export type NavigationVerdict = 'allow' | 'external' | 'refuse';
6
6
  export declare const appWindowNavigation: (url: string, appOrigin: string, schemes?: readonly string[]) => NavigationVerdict;
7
+ export declare const createGestureGate: (now?: () => number) => {
8
+ saw: (type: string) => void;
9
+ consume: () => boolean;
10
+ };
11
+ export type GestureGate = ReturnType<typeof createGestureGate>;
7
12
  export interface SenderFrame {
8
13
  readonly url: string;
9
14
  readonly parent: unknown;
@@ -34,6 +34,30 @@ export const appWindowNavigation = (url, appOrigin, schemes = []) => {
34
34
  }
35
35
  return isWebLink(url) ? 'external' : 'refuse';
36
36
  };
37
+ /* What a person does on purpose: a press or a key, never a move, a wheel or a scroll. */
38
+ const DELIBERATE_INPUT = new Set(['mouseDown', 'mouseUp', 'rawKeyDown', 'keyDown', 'gestureTap', 'touchEnd']);
39
+ /* How long after a press a link may still leave, which is longer than any click takes to navigate. */
40
+ const GESTURE_MS = 1000;
41
+ /*
42
+ * Whether a link a page wants to open in the system browser follows a press of the person. A page that
43
+ * runs scripts nobody vetted could otherwise send the person's browser anywhere, over and over. One
44
+ * press lets one link out. `saw` takes the `type` of each `input-event` of the page.
45
+ */
46
+ export const createGestureGate = (now = Date.now) => {
47
+ let pressedAt = null;
48
+ return {
49
+ saw: (type) => {
50
+ if (DELIBERATE_INPUT.has(type)) {
51
+ pressedAt = now();
52
+ }
53
+ },
54
+ consume: () => {
55
+ const recent = pressedAt !== null && now() - pressedAt <= GESTURE_MS;
56
+ pressedAt = null;
57
+ return recent;
58
+ }
59
+ };
60
+ };
37
61
  /*
38
62
  * Whether an IPC message came from the app itself: the top frame of an app window, still on the app's
39
63
  * origin. The web contents alone says nothing, since it stays the same object whatever it navigates to.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/shell",
3
- "version": "0.40.1",
3
+ "version": "0.40.2",
4
4
  "description": "The main process of an Electron app: its menu, its updater and the guards around its page.",
5
5
  "license": "FSL-1.1-MIT",
6
6
  "author": "Bas Milius",
@@ -3,6 +3,7 @@
3
3
  * a preload and a page read them as well as the main process.
4
4
  */
5
5
 
6
+ export { ipcSubscriber, type IpcListeners, type Subscribe } from './ipc.ts';
6
7
  export { MENU_ROLES, type MenuBarRole, type MenuNode, type MenuRole, type MenuSpec } from './menu.ts';
7
8
  export type { ThemeState } from './theme.ts';
8
9
  export type { UpdateErrorCode, UpdateState } from './update.ts';
@@ -0,0 +1,24 @@
1
+ /* The part of `ipcRenderer` a subscription uses, so this module imports nothing from Electron. */
2
+ export interface IpcListeners {
3
+ on(channel: string, listener: (event: unknown, ...args: unknown[]) => void): unknown;
4
+ removeListener(channel: string, listener: (event: unknown, ...args: unknown[]) => void): unknown;
5
+ }
6
+
7
+ export type Subscribe<Args extends unknown[]> = (listener: (...args: Args) => void) => () => void;
8
+
9
+ /*
10
+ * For a preload: turns a channel the main process sends on into what the page subscribes with, a
11
+ * listener in and what removes it again out. The event stays behind: it carries the sender, which the
12
+ * page must never hold.
13
+ */
14
+ export const ipcSubscriber =
15
+ (ipc: IpcListeners) =>
16
+ <Args extends unknown[]>(channel: string): Subscribe<Args> =>
17
+ (listener) => {
18
+ const handler = (_event: unknown, ...args: unknown[]): void => listener(...(args as Args));
19
+ ipc.on(channel, handler);
20
+
21
+ return () => {
22
+ ipc.removeListener(channel, handler);
23
+ };
24
+ };
package/src/index.ts CHANGED
@@ -6,8 +6,8 @@ export { createPageKeys } from './page-keys.ts';
6
6
  export type { PageKey, PageKeys } from './page-keys.ts';
7
7
  export { createUpdater } from './updater.ts';
8
8
  export type { Updater, UpdaterOptions } from './updater.ts';
9
- export { appWindowNavigation, isAppSender, isAppUrl, isExternalLink, isWebLink, originOf } from './web-guards.ts';
10
- export type { NavigationVerdict, SenderFrame } from './web-guards.ts';
9
+ export { appWindowNavigation, createGestureGate, isAppSender, isAppUrl, isExternalLink, isWebLink, originOf } from './web-guards.ts';
10
+ export type { GestureGate, NavigationVerdict, SenderFrame } from './web-guards.ts';
11
11
  export { createWindowState, fileStorage } from './window-state.ts';
12
12
  export type { SavedWindow, StateWindow, WindowDisplay, WindowSize, WindowState, WindowStateOptions, WindowStateStorage } from './window-state.ts';
13
13
  export { createTheme } from './theme.ts';
package/src/web-guards.ts CHANGED
@@ -44,6 +44,37 @@ export const appWindowNavigation = (url: string, appOrigin: string, schemes: rea
44
44
  return isWebLink(url) ? 'external' : 'refuse';
45
45
  };
46
46
 
47
+ /* What a person does on purpose: a press or a key, never a move, a wheel or a scroll. */
48
+ const DELIBERATE_INPUT: ReadonlySet<string> = new Set(['mouseDown', 'mouseUp', 'rawKeyDown', 'keyDown', 'gestureTap', 'touchEnd']);
49
+
50
+ /* How long after a press a link may still leave, which is longer than any click takes to navigate. */
51
+ const GESTURE_MS = 1000;
52
+
53
+ /*
54
+ * Whether a link a page wants to open in the system browser follows a press of the person. A page that
55
+ * runs scripts nobody vetted could otherwise send the person's browser anywhere, over and over. One
56
+ * press lets one link out. `saw` takes the `type` of each `input-event` of the page.
57
+ */
58
+ export const createGestureGate = (now: () => number = Date.now) => {
59
+ let pressedAt: number | null = null;
60
+
61
+ return {
62
+ saw: (type: string): void => {
63
+ if (DELIBERATE_INPUT.has(type)) {
64
+ pressedAt = now();
65
+ }
66
+ },
67
+ consume: (): boolean => {
68
+ const recent = pressedAt !== null && now() - pressedAt <= GESTURE_MS;
69
+ pressedAt = null;
70
+
71
+ return recent;
72
+ }
73
+ };
74
+ };
75
+
76
+ export type GestureGate = ReturnType<typeof createGestureGate>;
77
+
47
78
  /* The part of an IPC event's sender frame the check reads. */
48
79
  export interface SenderFrame {
49
80
  readonly url: string;