@flighthq/host-electron 0.5.1-next.920.da8f530 → 0.6.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 (137) hide show
  1. package/README.md +2 -2
  2. package/dist/contract.d.ts +19 -24
  3. package/dist/contract.d.ts.map +1 -1
  4. package/dist/contract.js +18 -24
  5. package/dist/contract.js.map +1 -1
  6. package/dist/electronApp.d.ts +62 -34
  7. package/dist/electronApp.d.ts.map +1 -1
  8. package/dist/electronApp.js +175 -234
  9. package/dist/electronApp.js.map +1 -1
  10. package/dist/electronClipboard.d.ts +7 -5
  11. package/dist/electronClipboard.d.ts.map +1 -1
  12. package/dist/electronClipboard.js +91 -66
  13. package/dist/electronClipboard.js.map +1 -1
  14. package/dist/electronDefaultHostGroups.d.ts +59 -0
  15. package/dist/electronDefaultHostGroups.d.ts.map +1 -0
  16. package/dist/electronDefaultHostGroups.js +109 -0
  17. package/dist/electronDefaultHostGroups.js.map +1 -0
  18. package/dist/electronDialog.d.ts +12 -9
  19. package/dist/electronDialog.d.ts.map +1 -1
  20. package/dist/electronDialog.js +28 -21
  21. package/dist/electronDialog.js.map +1 -1
  22. package/dist/electronIpc.d.ts +13 -11
  23. package/dist/electronIpc.d.ts.map +1 -1
  24. package/dist/electronIpc.js +32 -26
  25. package/dist/electronIpc.js.map +1 -1
  26. package/dist/electronMenu.d.ts +12 -6
  27. package/dist/electronMenu.d.ts.map +1 -1
  28. package/dist/electronMenu.js +41 -30
  29. package/dist/electronMenu.js.map +1 -1
  30. package/dist/electronMenuTemplate.d.ts +2 -1
  31. package/dist/electronMenuTemplate.d.ts.map +1 -1
  32. package/dist/electronMenuTemplate.js.map +1 -1
  33. package/dist/electronNotification.d.ts +25 -7
  34. package/dist/electronNotification.d.ts.map +1 -1
  35. package/dist/electronNotification.js +39 -16
  36. package/dist/electronNotification.js.map +1 -1
  37. package/dist/electronPlatform.d.ts +5 -3
  38. package/dist/electronPlatform.d.ts.map +1 -1
  39. package/dist/electronPlatform.js +9 -7
  40. package/dist/electronPlatform.js.map +1 -1
  41. package/dist/electronPower.d.ts +22 -11
  42. package/dist/electronPower.d.ts.map +1 -1
  43. package/dist/electronPower.js +48 -59
  44. package/dist/electronPower.js.map +1 -1
  45. package/dist/electronProtocol.d.ts +16 -8
  46. package/dist/electronProtocol.d.ts.map +1 -1
  47. package/dist/electronProtocol.js +45 -28
  48. package/dist/electronProtocol.js.map +1 -1
  49. package/dist/electronRegister.d.ts +5 -7
  50. package/dist/electronRegister.d.ts.map +1 -1
  51. package/dist/electronRegister.js +72 -99
  52. package/dist/electronRegister.js.map +1 -1
  53. package/dist/electronScreen.d.ts +7 -6
  54. package/dist/electronScreen.d.ts.map +1 -1
  55. package/dist/electronScreen.js +17 -14
  56. package/dist/electronScreen.js.map +1 -1
  57. package/dist/electronShell.d.ts +16 -8
  58. package/dist/electronShell.d.ts.map +1 -1
  59. package/dist/electronShell.js +50 -43
  60. package/dist/electronShell.js.map +1 -1
  61. package/dist/electronShortcut.d.ts +6 -4
  62. package/dist/electronShortcut.d.ts.map +1 -1
  63. package/dist/electronShortcut.js +16 -10
  64. package/dist/electronShortcut.js.map +1 -1
  65. package/dist/electronStorage.d.ts +5 -3
  66. package/dist/electronStorage.d.ts.map +1 -1
  67. package/dist/electronStorage.js +9 -7
  68. package/dist/electronStorage.js.map +1 -1
  69. package/dist/electronTray.d.ts +38 -21
  70. package/dist/electronTray.d.ts.map +1 -1
  71. package/dist/electronTray.js +110 -76
  72. package/dist/electronTray.js.map +1 -1
  73. package/dist/electronUpdater.d.ts +6 -3
  74. package/dist/electronUpdater.d.ts.map +1 -1
  75. package/dist/electronUpdater.js +10 -7
  76. package/dist/electronUpdater.js.map +1 -1
  77. package/dist/electronWindow.d.ts +23 -7
  78. package/dist/electronWindow.d.ts.map +1 -1
  79. package/dist/electronWindow.js +234 -152
  80. package/dist/electronWindow.js.map +1 -1
  81. package/dist/index.d.ts +19 -1
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +18 -1
  84. package/dist/index.js.map +1 -1
  85. package/dist/types/ElectronApi.d.ts +393 -0
  86. package/dist/types/ElectronApi.d.ts.map +1 -0
  87. package/dist/types/ElectronApi.js +20 -0
  88. package/dist/types/ElectronApi.js.map +1 -0
  89. package/dist/types/ElectronAppCapabilitiesFor.d.ts +8 -0
  90. package/dist/types/ElectronAppCapabilitiesFor.d.ts.map +1 -0
  91. package/dist/types/ElectronAppCapabilitiesFor.js +2 -0
  92. package/dist/types/ElectronAppCapabilitiesFor.js.map +1 -0
  93. package/dist/types/ElectronHost.d.ts +43 -0
  94. package/dist/types/ElectronHost.d.ts.map +1 -0
  95. package/dist/types/ElectronHost.js +2 -0
  96. package/dist/types/ElectronHost.js.map +1 -0
  97. package/dist/types/ElectronHostGroups.d.ts +85 -0
  98. package/dist/types/ElectronHostGroups.d.ts.map +1 -0
  99. package/dist/types/ElectronHostGroups.js +2 -0
  100. package/dist/types/ElectronHostGroups.js.map +1 -0
  101. package/dist/types/ElectronHostOptions.d.ts +7 -0
  102. package/dist/types/ElectronHostOptions.d.ts.map +1 -0
  103. package/dist/types/ElectronHostOptions.js +2 -0
  104. package/dist/types/ElectronHostOptions.js.map +1 -0
  105. package/dist/types/ElectronProtocolCapabilities.d.ts +3 -0
  106. package/dist/types/ElectronProtocolCapabilities.d.ts.map +1 -0
  107. package/dist/types/ElectronProtocolCapabilities.js +2 -0
  108. package/dist/types/ElectronProtocolCapabilities.js.map +1 -0
  109. package/dist/types/ElectronTrayCapabilitiesFor.d.ts +7 -0
  110. package/dist/types/ElectronTrayCapabilitiesFor.d.ts.map +1 -0
  111. package/dist/types/ElectronTrayCapabilitiesFor.js +2 -0
  112. package/dist/types/ElectronTrayCapabilitiesFor.js.map +1 -0
  113. package/dist/types/index.d.ts +7 -0
  114. package/dist/types/index.d.ts.map +1 -0
  115. package/dist/types/index.js +7 -0
  116. package/dist/types/index.js.map +1 -0
  117. package/package.json +42 -21
  118. package/src/electronApp.test.ts +158 -131
  119. package/src/electronClipboard.test.ts +40 -13
  120. package/src/electronDefaultHostGroups.test.ts +148 -0
  121. package/src/electronDialog.test.ts +55 -53
  122. package/src/electronHostTreeShaking.test.ts +51 -0
  123. package/src/electronIpc.test.ts +47 -66
  124. package/src/electronMenu.test.ts +61 -47
  125. package/src/electronMenuTemplate.test.ts +1 -1
  126. package/src/electronNotification.test.ts +71 -21
  127. package/src/electronPlatform.test.ts +17 -12
  128. package/src/electronPower.test.ts +112 -88
  129. package/src/electronProtocol.test.ts +68 -35
  130. package/src/electronRegister.test.ts +280 -26
  131. package/src/electronScreen.test.ts +34 -25
  132. package/src/electronShell.test.ts +99 -59
  133. package/src/electronShortcut.test.ts +22 -19
  134. package/src/electronStorage.test.ts +26 -24
  135. package/src/electronTray.test.ts +225 -109
  136. package/src/electronUpdater.test.ts +26 -19
  137. package/src/electronWindow.test.ts +229 -101
@@ -0,0 +1,148 @@
1
+ import {
2
+ webHostAudioDecode,
3
+ webHostImageDecode,
4
+ webHostImageEncode,
5
+ webHostTextShaper,
6
+ } from '@flighthq/host-web/contract';
7
+
8
+ import {
9
+ electronHostAccessibilityGroup,
10
+ electronHostAudioGroup,
11
+ electronHostBitmapGroup,
12
+ electronHostConnectivityGroup,
13
+ electronHostDeviceGroup,
14
+ electronHostFileSystemGroup,
15
+ electronHostFontGroup,
16
+ electronHostFullscreenGroup,
17
+ electronHostGeolocationGroup,
18
+ electronHostGlGroup,
19
+ electronHostGlyphGroup,
20
+ electronHostHapticsGroup,
21
+ electronHostAudioDecodeGroup,
22
+ electronHostImageDecodeGroup,
23
+ electronHostImageEncodeGroup,
24
+ electronHostImageGroup,
25
+ electronHostInputGroup,
26
+ electronHostLifecycleGroup,
27
+ electronHostMediaSessionGroup,
28
+ electronHostMidiGroup,
29
+ electronHostNetGroup,
30
+ electronHostPermissionsGroup,
31
+ electronHostSensorsGroup,
32
+ electronHostShareGroup,
33
+ electronHostSocketGroup,
34
+ electronHostSoftKeyboardGroup,
35
+ electronHostStatusBarGroup,
36
+ electronHostCanvasGroup,
37
+ electronHostSurfaceGroup,
38
+ electronHostTextSegmentGroup,
39
+ electronHostCompressGroup,
40
+ electronHostDecompressGroup,
41
+ electronHostTextShaperGroup,
42
+ electronHostVideoGroup,
43
+ electronHostWgpuGroup,
44
+ } from './electronDefaultHostGroups.ts';
45
+ import type { ElectronApi } from './types/index.ts';
46
+
47
+ const electron = {} as ElectronApi;
48
+
49
+ function emptyGroup(factory: () => object): () => void {
50
+ return () => {
51
+ it('constructs an exact empty capability group', () => {
52
+ expect(factory()).toEqual({});
53
+ });
54
+ };
55
+ }
56
+
57
+ const accessibilityGroup = emptyGroup(() => electronHostAccessibilityGroup(electron));
58
+ const audioGroup = emptyGroup(() => electronHostAudioGroup(electron));
59
+ const bitmapGroup = emptyGroup(() => electronHostBitmapGroup(electron));
60
+ const connectivityGroup = emptyGroup(() => electronHostConnectivityGroup(electron));
61
+ const deviceGroup = emptyGroup(() => electronHostDeviceGroup(electron));
62
+ const fileSystemGroup = emptyGroup(() => electronHostFileSystemGroup(electron));
63
+ const fontGroup = emptyGroup(() => electronHostFontGroup(electron));
64
+ const fullscreenGroup = emptyGroup(() => electronHostFullscreenGroup(electron));
65
+ const geolocationGroup = emptyGroup(() => electronHostGeolocationGroup(electron));
66
+ const glGroup = emptyGroup(() => electronHostGlGroup(electron));
67
+ const glyphGroup = emptyGroup(() => electronHostGlyphGroup(electron));
68
+ const hapticsGroup = emptyGroup(() => electronHostHapticsGroup(electron));
69
+ const imageGroup = emptyGroup(() => electronHostImageGroup(electron));
70
+ const inputGroup = emptyGroup(() => electronHostInputGroup(electron));
71
+ const lifecycleGroup = emptyGroup(() => electronHostLifecycleGroup(electron));
72
+ const mediaSessionGroup = emptyGroup(() => electronHostMediaSessionGroup(electron));
73
+ const midiGroup = emptyGroup(() => electronHostMidiGroup(electron));
74
+ const netGroup = emptyGroup(() => electronHostNetGroup(electron));
75
+ const permissionsGroup = emptyGroup(() => electronHostPermissionsGroup(electron));
76
+ const sensorsGroup = emptyGroup(() => electronHostSensorsGroup(electron));
77
+ const shareGroup = emptyGroup(() => electronHostShareGroup(electron));
78
+ const socketGroup = emptyGroup(() => electronHostSocketGroup(electron));
79
+ const softKeyboardGroup = emptyGroup(() => electronHostSoftKeyboardGroup(electron));
80
+ const statusBarGroup = emptyGroup(() => electronHostStatusBarGroup(electron));
81
+ const canvasGroup = emptyGroup(() => electronHostCanvasGroup(electron));
82
+ const surfaceGroup = emptyGroup(() => electronHostSurfaceGroup(electron));
83
+ const textSegmentGroup = emptyGroup(() => electronHostTextSegmentGroup(electron));
84
+ const compressGroup = emptyGroup(() => electronHostCompressGroup(electron));
85
+ const decompressGroup = emptyGroup(() => electronHostDecompressGroup(electron));
86
+ const videoGroup = emptyGroup(() => electronHostVideoGroup(electron));
87
+ const wgpuGroup = emptyGroup(() => electronHostWgpuGroup(electron));
88
+
89
+ describe('electronHostAccessibilityGroup', accessibilityGroup);
90
+ describe('electronHostAudioDecodeGroup', () => {
91
+ it('delegates to the shared web audio decode capabilities', () => {
92
+ expect(electronHostAudioDecodeGroup(electron)).toBe(webHostAudioDecode);
93
+ });
94
+ });
95
+ describe('electronHostAudioGroup', audioGroup);
96
+ describe('electronHostBitmapGroup', bitmapGroup);
97
+ describe('electronHostCanvasGroup', canvasGroup);
98
+ describe('electronHostCompressGroup', compressGroup);
99
+ describe('electronHostConnectivityGroup', connectivityGroup);
100
+ describe('electronHostDecompressGroup', decompressGroup);
101
+ describe('electronHostDeviceGroup', deviceGroup);
102
+ describe('electronHostFileSystemGroup', fileSystemGroup);
103
+ describe('electronHostFontGroup', fontGroup);
104
+ describe('electronHostFullscreenGroup', fullscreenGroup);
105
+ describe('electronHostGeolocationGroup', geolocationGroup);
106
+ describe('electronHostGlGroup', glGroup);
107
+ describe('electronHostGlyphGroup', glyphGroup);
108
+ describe('electronHostHapticsGroup', hapticsGroup);
109
+ describe('electronHostImageDecodeGroup', () => {
110
+ it('delegates to the shared web image decode capabilities', () => {
111
+ expect(electronHostImageDecodeGroup(electron)).toBe(webHostImageDecode);
112
+ });
113
+ });
114
+ describe('electronHostImageEncodeGroup', () => {
115
+ it('delegates to the shared web image encode capabilities', () => {
116
+ expect(electronHostImageEncodeGroup(electron)).toBe(webHostImageEncode);
117
+ });
118
+ });
119
+ describe('electronHostImageGroup', imageGroup);
120
+ describe('electronHostInputGroup', inputGroup);
121
+ describe('electronHostLifecycleGroup', lifecycleGroup);
122
+ describe('electronHostMediaSessionGroup', mediaSessionGroup);
123
+ describe('electronHostMidiGroup', midiGroup);
124
+ describe('electronHostNetGroup', netGroup);
125
+ describe('electronHostPermissionsGroup', permissionsGroup);
126
+ describe('electronHostSensorsGroup', sensorsGroup);
127
+ describe('electronHostShareGroup', shareGroup);
128
+ describe('electronHostSocketGroup', socketGroup);
129
+ describe('electronHostSoftKeyboardGroup', softKeyboardGroup);
130
+ describe('electronHostStatusBarGroup', statusBarGroup);
131
+ describe('electronHostSurfaceGroup', surfaceGroup);
132
+
133
+ describe('electronHostTextSegmentGroup', textSegmentGroup);
134
+ describe('electronHostTextShaperGroup', () => {
135
+ it('fills the shaper slot with the shared web text shaper', () => {
136
+ const group = electronHostTextShaperGroup(electron);
137
+ expect(group.shaper).toBe(webHostTextShaper);
138
+ });
139
+
140
+ it('is plain data with no Entity runtime and no symbol keys', () => {
141
+ const group = electronHostTextShaperGroup(electron);
142
+ expect(Symbol.for('EntityRuntime') in group).toBe(false);
143
+ expect(Object.getOwnPropertySymbols(group)).toEqual([]);
144
+ expect(Object.keys(group)).toEqual(['shaper']);
145
+ });
146
+ });
147
+ describe('electronHostVideoGroup', videoGroup);
148
+ describe('electronHostWgpuGroup', wgpuGroup);
@@ -1,21 +1,20 @@
1
+ import {
2
+ electronHostDialog,
3
+ electronHostDirectoryOpenDialog,
4
+ electronHostFileOpenDialog,
5
+ electronHostFileSaveDialog,
6
+ electronHostMessageDialog,
7
+ populateElectronHostDirectoryOpenDialog,
8
+ populateElectronHostFileOpenDialog,
9
+ populateElectronHostFileSaveDialog,
10
+ populateElectronHostMessageDialog,
11
+ } from './electronDialog.ts';
1
12
  import type {
2
13
  ElectronApi,
3
14
  ElectronMessageBoxOptions,
4
15
  ElectronOpenDialogOptions,
5
16
  ElectronSaveDialogOptions,
6
- } from '@flighthq/types/contract';
7
- import { EntityRuntimeKey } from '@flighthq/types/contract';
8
-
9
- import {
10
- createElectronDirectoryOpenDialogBackend,
11
- createElectronFileOpenDialogBackend,
12
- createElectronFileSaveDialogBackend,
13
- createElectronMessageDialogBackend,
14
- initializeElectronDirectoryOpenDialogBackend,
15
- initializeElectronFileOpenDialogBackend,
16
- initializeElectronFileSaveDialogBackend,
17
- initializeElectronMessageDialogBackend,
18
- } from './electronDialog';
17
+ } from './types/index.ts';
19
18
 
20
19
  function fakeElectron(overrides: {
21
20
  open?: { canceled: boolean; filePaths: string[] } | Error;
@@ -48,37 +47,42 @@ function fakeElectron(overrides: {
48
47
  return { electron, calls };
49
48
  }
50
49
 
51
- describe('createElectronDirectoryOpenDialogBackend', () => {
50
+ describe('electronHostDialog', () => {
51
+ it('constructs the exact dialog group', () => {
52
+ const dialog = electronHostDialog(fakeElectron({}).electron);
53
+ expect(Object.keys(dialog).sort()).toEqual(['directoryOpen', 'fileOpen', 'fileSave', 'message']);
54
+ });
55
+ });
56
+
57
+ describe('electronHostDirectoryOpenDialog', () => {
52
58
  it('uses a method-tight single-directory operation', async () => {
53
59
  const { electron, calls } = fakeElectron({ open: { canceled: false, filePaths: ['/docs'] } });
54
- const result = await createElectronDirectoryOpenDialogBackend(electron).open();
60
+ const result = await electronHostDirectoryOpenDialog(electron).open();
55
61
  expect(calls.openOptions).toEqual({ properties: ['openDirectory'] });
56
62
  expect(result.outcome === 'selected' ? result.handle.path : null).toBe('/docs');
57
63
  });
58
64
  });
59
65
 
60
- describe('createElectronFileOpenDialogBackend', () => {
61
- it('exposes independent Entity providers', () => {
66
+ describe('electronHostFileOpenDialog', () => {
67
+ it('exposes independent providers', () => {
62
68
  const electron = fakeElectron({}).electron;
63
69
  const providers = [
64
- createElectronDirectoryOpenDialogBackend(electron),
65
- createElectronFileOpenDialogBackend(electron),
66
- createElectronFileSaveDialogBackend(electron),
70
+ electronHostDirectoryOpenDialog(electron),
71
+ electronHostFileOpenDialog(electron),
72
+ electronHostFileSaveDialog(electron),
67
73
  ];
68
- expect(providers.every((provider) => EntityRuntimeKey in provider)).toBe(true);
69
74
  expect(new Set(providers).size).toBe(3);
70
75
  });
71
76
 
72
77
  it('returns selected nonempty file handles and maps common filters/multiple', async () => {
73
78
  const { electron, calls } = fakeElectron({ open: { canceled: false, filePaths: ['/a', '/b'] } });
74
- const result = await createElectronFileOpenDialogBackend(electron).open({
79
+ const result = await electronHostFileOpenDialog(electron).open({
75
80
  filters: [{ accept: { 'text/plain': ['.txt'], 'text/markdown': ['md'] }, name: 'Text' }],
76
81
  multiple: true,
77
82
  });
78
83
  expect(result.outcome).toBe('selected');
79
84
  if (result.outcome === 'selected') {
80
85
  expect(result.handles.map((handle) => handle.name)).toEqual(['a', 'b']);
81
- expect(result.handles.every((handle) => EntityRuntimeKey in handle)).toBe(true);
82
86
  }
83
87
  expect(calls.openOptions).toEqual({
84
88
  filters: [{ extensions: ['txt', 'md'], name: 'Text' }],
@@ -88,21 +92,19 @@ describe('createElectronFileOpenDialogBackend', () => {
88
92
 
89
93
  it('uses the independent single-directory operation and preserves a root basename', async () => {
90
94
  const { electron, calls } = fakeElectron({ open: { canceled: false, filePaths: ['/'] } });
91
- const result = await createElectronDirectoryOpenDialogBackend(electron).open();
95
+ const result = await electronHostDirectoryOpenDialog(electron).open();
92
96
  expect(calls.openOptions).toEqual({ properties: ['openDirectory'] });
93
97
  expect(result.outcome === 'selected' ? result.handle.name : null).toBe('/');
94
98
  });
95
99
 
96
100
  it('distinguishes cancel, security denial, and operation failure', async () => {
97
- const cancelled = await createElectronFileOpenDialogBackend(
101
+ const cancelled = await electronHostFileOpenDialog(
98
102
  fakeElectron({ open: { canceled: true, filePaths: ['/ignored'] } }).electron,
99
103
  ).open({});
100
- const security = await createElectronFileOpenDialogBackend(
104
+ const security = await electronHostFileOpenDialog(
101
105
  fakeElectron({ open: Object.assign(new Error('denied'), { code: 'EACCES' }) }).electron,
102
106
  ).open({});
103
- const failed = await createElectronFileOpenDialogBackend(fakeElectron({ open: new Error('broken') }).electron).open(
104
- {},
105
- );
107
+ const failed = await electronHostFileOpenDialog(fakeElectron({ open: new Error('broken') }).electron).open({});
106
108
  expect(cancelled.outcome).toBe('cancelled');
107
109
  expect(security.outcome).toBe('security-denied');
108
110
  expect(failed.outcome).toBe('file-open-failed');
@@ -110,81 +112,81 @@ describe('createElectronFileOpenDialogBackend', () => {
110
112
 
111
113
  it('maps the common defaultName invariant and returns a selected save handle', async () => {
112
114
  const { electron, calls } = fakeElectron({ save: { canceled: false, filePath: '/out.txt' } });
113
- const result = await createElectronFileSaveDialogBackend(electron).save({ defaultName: 'suggested.txt' });
115
+ const result = await electronHostFileSaveDialog(electron).save({ defaultName: 'suggested.txt' });
114
116
  expect(calls.saveOptions?.defaultPath).toBe('suggested.txt');
115
117
  expect(result.outcome === 'selected' ? result.handle.path : null).toBe('/out.txt');
116
118
  });
117
119
 
118
120
  it('reports runtime-unavailable when an Electron dialog method is absent', async () => {
119
121
  const electron = { dialog: {} } as ElectronApi;
120
- expect((await createElectronDirectoryOpenDialogBackend(electron).open()).outcome).toBe('runtime-unavailable');
121
- expect((await createElectronFileOpenDialogBackend(electron).open({})).outcome).toBe('runtime-unavailable');
122
- expect((await createElectronFileSaveDialogBackend(electron).save({})).outcome).toBe('runtime-unavailable');
122
+ expect((await electronHostDirectoryOpenDialog(electron).open()).outcome).toBe('runtime-unavailable');
123
+ expect((await electronHostFileOpenDialog(electron).open({})).outcome).toBe('runtime-unavailable');
124
+ expect((await electronHostFileSaveDialog(electron).save({})).outcome).toBe('runtime-unavailable');
123
125
  });
124
126
 
125
127
  it('does not open a native picker when already aborted', async () => {
126
128
  const { electron, calls } = fakeElectron({ open: { canceled: false, filePaths: ['/ignored'] } });
127
129
  const controller = new AbortController();
128
130
  controller.abort();
129
- await expect(createElectronFileOpenDialogBackend(electron).open({ signal: controller.signal })).resolves.toEqual({
131
+ await expect(electronHostFileOpenDialog(electron).open({ signal: controller.signal })).resolves.toEqual({
130
132
  outcome: 'cancelled',
131
133
  });
132
134
  expect(calls.openOptions).toBeUndefined();
133
135
  });
134
136
  });
135
137
 
136
- describe('createElectronFileSaveDialogBackend', () => {
138
+ describe('electronHostFileSaveDialog', () => {
137
139
  it('maps the common default name and returns one selected handle', async () => {
138
140
  const { electron, calls } = fakeElectron({ save: { canceled: false, filePath: '/saved.txt' } });
139
- const result = await createElectronFileSaveDialogBackend(electron).save({ defaultName: 'suggested.txt' });
141
+ const result = await electronHostFileSaveDialog(electron).save({ defaultName: 'suggested.txt' });
140
142
  expect(calls.saveOptions?.defaultPath).toBe('suggested.txt');
141
143
  expect(result.outcome === 'selected' ? result.handle.path : null).toBe('/saved.txt');
142
144
  });
143
145
  });
144
146
 
145
- describe('createElectronMessageDialogBackend', () => {
147
+ describe('electronHostMessageDialog', () => {
146
148
  it('maps message results to button index, cancelled, and checkbox state', async () => {
147
149
  const { electron, calls } = fakeElectron({ message: { response: 2, checkboxChecked: true } });
148
- const result = await createElectronMessageDialogBackend(electron).message({ message: 'hi', kind: 'warning' });
150
+ const result = await electronHostMessageDialog(electron).message({ message: 'hi', kind: 'warning' });
149
151
  expect(result).toEqual({ buttonIndex: 2, cancelled: false, checkboxChecked: true });
150
152
  expect(calls.messageOptions?.type).toBe('warning');
151
153
  });
152
154
 
153
155
  it('confirm returns true only when the OK button is chosen', async () => {
154
156
  const yes = fakeElectron({ message: { response: 0, checkboxChecked: false } });
155
- expect(await createElectronMessageDialogBackend(yes.electron).confirm({ message: 'ok?' })).toBe(true);
157
+ expect(await electronHostMessageDialog(yes.electron).confirm({ message: 'ok?' })).toBe(true);
156
158
  const no = fakeElectron({ message: { response: 1, checkboxChecked: false } });
157
- expect(await createElectronMessageDialogBackend(no.electron).confirm({ message: 'ok?' })).toBe(false);
159
+ expect(await electronHostMessageDialog(no.electron).confirm({ message: 'ok?' })).toBe(false);
158
160
  expect(no.calls.messageOptions?.buttons).toEqual(['OK', 'Cancel']);
159
161
  });
160
162
 
161
163
  it('forwards the signal supported by Electron message boxes', async () => {
162
164
  const { electron, calls } = fakeElectron({});
163
165
  const signal = new AbortController().signal;
164
- await createElectronMessageDialogBackend(electron).message({ message: 'wait', signal });
166
+ await electronHostMessageDialog(electron).message({ message: 'wait', signal });
165
167
  expect(calls.messageOptions?.signal).toBe(signal);
166
168
  });
167
169
  });
168
- describe('initializeElectronDirectoryOpenDialogBackend', () => {
169
- it('is the construction initializer of createElectronDirectoryOpenDialogBackend', () => {
170
- expect(typeof initializeElectronDirectoryOpenDialogBackend).toBe('function');
170
+ describe('populateElectronHostDirectoryOpenDialog', () => {
171
+ it('is the construction initializer of electronHostDirectoryOpenDialog', () => {
172
+ expect(typeof populateElectronHostDirectoryOpenDialog).toBe('function');
171
173
  });
172
174
  });
173
175
 
174
- describe('initializeElectronFileOpenDialogBackend', () => {
175
- it('is the construction initializer of createElectronFileOpenDialogBackend', () => {
176
- expect(typeof initializeElectronFileOpenDialogBackend).toBe('function');
176
+ describe('populateElectronHostFileOpenDialog', () => {
177
+ it('is the construction initializer of electronHostFileOpenDialog', () => {
178
+ expect(typeof populateElectronHostFileOpenDialog).toBe('function');
177
179
  });
178
180
  });
179
181
 
180
- describe('initializeElectronFileSaveDialogBackend', () => {
181
- it('is the construction initializer of createElectronFileSaveDialogBackend', () => {
182
- expect(typeof initializeElectronFileSaveDialogBackend).toBe('function');
182
+ describe('populateElectronHostFileSaveDialog', () => {
183
+ it('is the construction initializer of electronHostFileSaveDialog', () => {
184
+ expect(typeof populateElectronHostFileSaveDialog).toBe('function');
183
185
  });
184
186
  });
185
187
 
186
- describe('initializeElectronMessageDialogBackend', () => {
187
- it('is the construction initializer of createElectronMessageDialogBackend', () => {
188
- expect(typeof initializeElectronMessageDialogBackend).toBe('function');
188
+ describe('populateElectronHostMessageDialog', () => {
189
+ it('is the construction initializer of electronHostMessageDialog', () => {
190
+ expect(typeof populateElectronHostMessageDialog).toBe('function');
189
191
  });
190
192
  });
@@ -0,0 +1,51 @@
1
+ // @vitest-environment node
2
+
3
+ import { build } from 'esbuild';
4
+
5
+ describe('Electron Host tree shaking', () => {
6
+ it('keeps a direct App leaf independent of sibling and full-host assembly', async () => {
7
+ const output = await bundleElectronHostExport('electronHostAppName');
8
+ expect(output).toContain('electronHostAppName');
9
+ expect(output).toContain('getName');
10
+ expect(output).not.toContain('getLocale');
11
+ expect(output).not.toContain('requestSingleInstanceLock');
12
+ expect(output).not.toContain('availableFormats');
13
+ });
14
+
15
+ it('keeps the Clipboard text leaf independent of sibling clipboard providers', async () => {
16
+ const output = await bundleElectronHostExport('electronHostClipboardText');
17
+ expect(output).toContain('electronHostClipboardText');
18
+ expect(output).toContain('readText');
19
+ expect(output).toContain('writeText');
20
+ expect(output).not.toContain('availableFormats');
21
+ expect(output).not.toContain('nativeImage');
22
+ expect(output).not.toContain('readBookmark');
23
+ expect(output).not.toContain('readRTF');
24
+ });
25
+ });
26
+
27
+ async function bundleElectronHostExport(name: string): Promise<string> {
28
+ const result = await build({
29
+ bundle: true,
30
+ format: 'esm',
31
+ logLevel: 'silent',
32
+ minify: true,
33
+ packages: 'external',
34
+ platform: 'node',
35
+ stdin: {
36
+ contents: `export { ${name} } from './index.ts';`,
37
+ resolveDir: fileUrlDirectory(import.meta.url),
38
+ sourcefile: `tree-shake-${name}.ts`,
39
+ },
40
+ treeShaking: true,
41
+ write: false,
42
+ });
43
+ return result.outputFiles[0].text;
44
+ }
45
+
46
+ function fileUrlDirectory(url: string): string {
47
+ const directory = new URL('.', url);
48
+ const pathname = decodeURIComponent(directory.pathname);
49
+ if (directory.hostname !== '') return `//${directory.hostname}${pathname}`;
50
+ return /^\/[A-Za-z]:\//.test(pathname) ? pathname.slice(1) : pathname;
51
+ }
@@ -1,18 +1,17 @@
1
- import type { ElectronApi, ElectronIpcRenderer, ElectronIpcTarget, Entity } from '@flighthq/types/contract';
2
- import { EntityRuntimeKey } from '@flighthq/types/contract';
3
-
4
1
  import {
5
- createElectronIpcHandleBackend,
6
- createElectronIpcInvokeBackend,
7
- createElectronIpcMessageBackend,
8
- createElectronIpcSendBackend,
9
- createElectronIpcTargetedSendBackend,
10
- initializeElectronIpcHandleBackend,
11
- initializeElectronIpcInvokeBackend,
12
- initializeElectronIpcMessageBackend,
13
- initializeElectronIpcSendBackend,
14
- initializeElectronIpcTargetedSendBackend,
15
- } from './electronIpc';
2
+ electronHostIpc,
3
+ electronHostIpcHandle,
4
+ electronHostIpcInvoke,
5
+ electronHostIpcMessage,
6
+ electronHostIpcSend,
7
+ electronHostIpcTargetedSend,
8
+ populateElectronHostIpcHandle,
9
+ populateElectronHostIpcInvoke,
10
+ populateElectronHostIpcMessage,
11
+ populateElectronHostIpcSend,
12
+ populateElectronHostIpcTargetedSend,
13
+ } from './electronIpc.ts';
14
+ import type { ElectronApi, ElectronIpcRenderer, ElectronIpcTarget } from './types/index.ts';
16
15
 
17
16
  function fakeElectron(): {
18
17
  channels: Map<string, Set<(event: unknown, ...args: unknown[]) => void>>;
@@ -63,10 +62,17 @@ function fakeRenderer(): {
63
62
  };
64
63
  }
65
64
 
66
- describe('createElectronIpcHandleBackend', () => {
65
+ describe('electronHostIpc', () => {
66
+ it('constructs the main-process IPC group', () => {
67
+ const ipc = electronHostIpc(fakeElectron().electron);
68
+ expect(Object.keys(ipc).sort()).toEqual(['handle', 'message', 'targetedSend']);
69
+ });
70
+ });
71
+
72
+ describe('electronHostIpcHandle', () => {
67
73
  it('registers a main-process handler, drops the event, and releases it idempotently', async () => {
68
74
  const { electron, handlers } = fakeElectron();
69
- const backend = createElectronIpcHandleBackend(electron);
75
+ const backend = electronHostIpcHandle(electron);
70
76
  const stop = backend.handle('double', (value) => (value as number) * 2);
71
77
 
72
78
  expect(await handlers.get('double')?.({ sender: 1 }, 4)).toBe(8);
@@ -75,37 +81,22 @@ describe('createElectronIpcHandleBackend', () => {
75
81
  stop();
76
82
  expect(handlers.has('double')).toBe(false);
77
83
  });
78
-
79
- it('returns an Entity', () => {
80
- const backend: Entity = createElectronIpcHandleBackend(fakeElectron().electron);
81
- expect(EntityRuntimeKey in backend).toBe(true);
82
- });
83
84
  });
84
85
 
85
- describe('createElectronIpcInvokeBackend', () => {
86
+ describe('electronHostIpcInvoke', () => {
86
87
  it('invokes from the renderer with spread arguments and returns the response', async () => {
87
88
  const { invocations, renderer } = fakeRenderer();
88
- const backend = createElectronIpcInvokeBackend(renderer);
89
+ const backend = electronHostIpcInvoke(renderer);
89
90
 
90
91
  await expect(backend.invoke('compute', [1, 2])).resolves.toEqual({ args: [1, 2], channel: 'compute' });
91
92
  expect(invocations).toEqual([{ args: [1, 2], channel: 'compute' }]);
92
93
  });
93
-
94
- it('returns an Entity', () => {
95
- const backend: Entity = createElectronIpcInvokeBackend(fakeRenderer().renderer);
96
- expect(EntityRuntimeKey in backend).toBe(true);
97
- });
98
94
  });
99
95
 
100
- describe('createElectronIpcMessageBackend', () => {
101
- it('returns an Entity in both runtime and type', () => {
102
- const backend: Entity = createElectronIpcMessageBackend(fakeElectron().electron);
103
- expect(EntityRuntimeKey in backend).toBe(true);
104
- });
105
-
96
+ describe('electronHostIpcMessage', () => {
106
97
  it('delivers a renderer message with its arguments, dropping the event object', () => {
107
98
  const { electron, channels } = fakeElectron();
108
- const backend = createElectronIpcMessageBackend(electron);
99
+ const backend = electronHostIpcMessage(electron);
109
100
  const seen: readonly unknown[][] = [];
110
101
  backend.subscribe('ping', (args) => (seen as unknown[][]).push([...args]));
111
102
  for (const listener of channels.get('ping') ?? []) listener({ sender: 1 }, 'a', 2);
@@ -116,7 +107,7 @@ describe('createElectronIpcMessageBackend', () => {
116
107
  // provider-level destroy: ipcMain is the caller's to tear down, not this backend's.
117
108
  it('removes exactly its own ipcMain listener on unsubscribe', () => {
118
109
  const { electron, channels } = fakeElectron();
119
- const backend = createElectronIpcMessageBackend(electron);
110
+ const backend = electronHostIpcMessage(electron);
120
111
  const stopFirst = backend.subscribe('ping', () => {});
121
112
  backend.subscribe('ping', () => {});
122
113
  expect(channels.get('ping')?.size).toBe(2);
@@ -125,7 +116,7 @@ describe('createElectronIpcMessageBackend', () => {
125
116
  });
126
117
 
127
118
  it('remains independent from the other IPC capabilities', () => {
128
- const backend = createElectronIpcMessageBackend(fakeElectron().electron);
119
+ const backend = electronHostIpcMessage(fakeElectron().electron);
129
120
  expect(Object.keys(backend).filter((key) => key !== 'constructor')).toEqual(['subscribe']);
130
121
  expect('send' in backend).toBe(false);
131
122
  expect('invoke' in backend).toBe(false);
@@ -135,23 +126,18 @@ describe('createElectronIpcMessageBackend', () => {
135
126
  });
136
127
  });
137
128
 
138
- describe('createElectronIpcSendBackend', () => {
129
+ describe('electronHostIpcSend', () => {
139
130
  it('sends from the renderer with spread arguments', () => {
140
131
  const { renderer, sent } = fakeRenderer();
141
- const backend = createElectronIpcSendBackend(renderer);
132
+ const backend = electronHostIpcSend(renderer);
142
133
 
143
134
  backend.send('log', ['hello', 7]);
144
135
 
145
136
  expect(sent).toEqual([{ args: ['hello', 7], channel: 'log' }]);
146
137
  });
147
-
148
- it('returns an Entity', () => {
149
- const backend: Entity = createElectronIpcSendBackend(fakeRenderer().renderer);
150
- expect(EntityRuntimeKey in backend).toBe(true);
151
- });
152
138
  });
153
139
 
154
- describe('createElectronIpcTargetedSendBackend', () => {
140
+ describe('electronHostIpcTargetedSend', () => {
155
141
  it('keeps the target generic and delegates to its Electron send seam', () => {
156
142
  const sent: Array<{ readonly args: readonly unknown[]; readonly channel: string }> = [];
157
143
  const target: ElectronIpcTarget & { readonly label: string } = {
@@ -160,44 +146,39 @@ describe('createElectronIpcTargetedSendBackend', () => {
160
146
  sent.push({ args, channel });
161
147
  },
162
148
  };
163
- const backend = createElectronIpcTargetedSendBackend<typeof target>();
149
+ const backend = electronHostIpcTargetedSend<typeof target>();
164
150
 
165
151
  backend.send(target, 'refresh', [1, 2]);
166
152
 
167
153
  expect(sent).toEqual([{ args: [1, 2], channel: 'refresh' }]);
168
154
  });
169
-
170
- it('returns an Entity', () => {
171
- const backend: Entity = createElectronIpcTargetedSendBackend<ElectronIpcTarget>();
172
- expect(EntityRuntimeKey in backend).toBe(true);
173
- });
174
155
  });
175
- describe('initializeElectronIpcHandleBackend', () => {
176
- it('is the construction initializer of createElectronIpcHandleBackend', () => {
177
- expect(typeof initializeElectronIpcHandleBackend).toBe('function');
156
+ describe('populateElectronHostIpcHandle', () => {
157
+ it('is the construction initializer of electronHostIpcHandle', () => {
158
+ expect(typeof populateElectronHostIpcHandle).toBe('function');
178
159
  });
179
160
  });
180
161
 
181
- describe('initializeElectronIpcInvokeBackend', () => {
182
- it('is the construction initializer of createElectronIpcInvokeBackend', () => {
183
- expect(typeof initializeElectronIpcInvokeBackend).toBe('function');
162
+ describe('populateElectronHostIpcInvoke', () => {
163
+ it('is the construction initializer of electronHostIpcInvoke', () => {
164
+ expect(typeof populateElectronHostIpcInvoke).toBe('function');
184
165
  });
185
166
  });
186
167
 
187
- describe('initializeElectronIpcMessageBackend', () => {
188
- it('is the construction initializer of createElectronIpcMessageBackend', () => {
189
- expect(typeof initializeElectronIpcMessageBackend).toBe('function');
168
+ describe('populateElectronHostIpcMessage', () => {
169
+ it('is the construction initializer of electronHostIpcMessage', () => {
170
+ expect(typeof populateElectronHostIpcMessage).toBe('function');
190
171
  });
191
172
  });
192
173
 
193
- describe('initializeElectronIpcSendBackend', () => {
194
- it('is the construction initializer of createElectronIpcSendBackend', () => {
195
- expect(typeof initializeElectronIpcSendBackend).toBe('function');
174
+ describe('populateElectronHostIpcSend', () => {
175
+ it('is the construction initializer of electronHostIpcSend', () => {
176
+ expect(typeof populateElectronHostIpcSend).toBe('function');
196
177
  });
197
178
  });
198
179
 
199
- describe('initializeElectronIpcTargetedSendBackend', () => {
200
- it('is the construction initializer of createElectronIpcTargetedSendBackend', () => {
201
- expect(typeof initializeElectronIpcTargetedSendBackend).toBe('function');
180
+ describe('populateElectronHostIpcTargetedSend', () => {
181
+ it('is the construction initializer of electronHostIpcTargetedSend', () => {
182
+ expect(typeof populateElectronHostIpcTargetedSend).toBe('function');
202
183
  });
203
184
  });