crossbind 2.0.0-beta.68 → 2.0.0-beta.72

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 (88) hide show
  1. package/README.md +1 -1
  2. package/package.json +70 -59
  3. package/src/actions/addExtension.js +196 -0
  4. package/src/actions/androidGradle.js +26 -17
  5. package/src/actions/appInstaller.js +69 -153
  6. package/src/actions/appSwift.js +66 -0
  7. package/src/actions/buildApp.js +177 -74
  8. package/src/actions/buildDependencies.js +3 -3
  9. package/src/actions/buildNode.js +6 -1
  10. package/src/actions/buildWasm.js +0 -9
  11. package/src/actions/createImportedBridges.js +10 -8
  12. package/src/actions/createInterface.js +40 -29
  13. package/src/actions/createLib.js +3 -7
  14. package/src/actions/extensionApi.js +298 -0
  15. package/src/actions/extensionCommands.js +41 -0
  16. package/src/actions/extensionsInApp.js +19 -0
  17. package/src/actions/getAllBridges.js +20 -5
  18. package/src/actions/getDependLibs.js +5 -0
  19. package/src/actions/getLinkInputs.js +2 -1
  20. package/src/actions/gradleProject.js +207 -16
  21. package/src/actions/iosDevice.js +2 -2
  22. package/src/actions/iosSimulator.js +3 -3
  23. package/src/actions/licenses.js +113 -58
  24. package/src/actions/linuxPackages.js +48 -0
  25. package/src/actions/mavenVerification.js +37 -0
  26. package/src/actions/{prepareConanDependencies.js → preparePackageSources.js} +13 -12
  27. package/src/actions/publish.js +220 -0
  28. package/src/actions/windowsSigning.js +1 -1
  29. package/src/actions/writeRuntimeEntry.js +2 -2
  30. package/src/actions/xcodeProject.js +60 -29
  31. package/src/actions/xcodeTargets.js +122 -0
  32. package/src/actions/xcodebuild.js +65 -0
  33. package/src/app/index.d.ts +144 -132
  34. package/src/app/index.js +147 -185
  35. package/src/assets/configure/install.sh +0 -0
  36. package/src/assets/node-import-hooks.mjs +7 -6
  37. package/src/assets/toolchain-digests.json +20 -20
  38. package/src/bin.js +37 -37
  39. package/src/index.js +4 -3
  40. package/src/integration/getCrossbindScript.js +3 -2
  41. package/src/integration/getDependFilePath.js +10 -10
  42. package/src/program.js +32 -3
  43. package/src/state/{attachConanDependencies.js → attachPackageSources.js} +20 -16
  44. package/src/state/index.js +8 -6
  45. package/src/state/loadConfig.js +26 -9
  46. package/src/state/refreshPackageSources.js +19 -0
  47. package/src/utils/appExtensions.js +777 -0
  48. package/src/utils/appFiles.js +58 -0
  49. package/src/utils/appLaunches.js +249 -0
  50. package/src/utils/appOptions.js +142 -112
  51. package/src/utils/appRustCrates.js +2 -2
  52. package/src/utils/appTargets.js +168 -0
  53. package/src/utils/bridgeTools.js +71 -0
  54. package/src/utils/configChanges.js +30 -0
  55. package/src/utils/configWriter.js +143 -26
  56. package/src/utils/cppDts.js +11 -11
  57. package/src/utils/extensionSteps.js +172 -0
  58. package/src/utils/nativeImports.js +9 -2
  59. package/src/utils/nodeDevBuild.js +2 -1
  60. package/src/utils/openUrl.js +2 -1
  61. package/src/utils/packageSourceApi.js +87 -0
  62. package/src/utils/packageSourceImport.js +69 -0
  63. package/src/utils/packageSourceInstall.js +162 -0
  64. package/src/utils/{conanStage.js → packageSourceStage.js} +64 -90
  65. package/src/utils/packageSources.js +108 -0
  66. package/src/utils/plist.js +5 -0
  67. package/src/utils/printableText.js +5 -0
  68. package/src/utils/resolveAppHost.js +7 -0
  69. package/src/utils/runPackageSource.js +251 -0
  70. package/src/utils/rustBridgeGen.js +3 -3
  71. package/src/utils/systemConfig.js +16 -3
  72. package/src/utils/targetToolchain.js +77 -0
  73. package/vitest.config.js +6 -1
  74. package/src/actions/appSetup.js +0 -116
  75. package/src/actions/appStoreConnect.js +0 -188
  76. package/src/actions/appUpdate.js +0 -68
  77. package/src/actions/extensions.js +0 -7
  78. package/src/actions/googlePlay.js +0 -213
  79. package/src/actions/googleSignIn.js +0 -78
  80. package/src/actions/playSetup.js +0 -19
  81. package/src/actions/publishAndroid.js +0 -102
  82. package/src/actions/publishIos.js +0 -146
  83. package/src/state/refreshConanDependencies.js +0 -17
  84. package/src/utils/conanDependencies.js +0 -66
  85. package/src/utils/conanImport.js +0 -58
  86. package/src/utils/conanInstall.js +0 -154
  87. package/src/utils/conanProfile.js +0 -203
  88. package/src/utils/runConan.js +0 -257
package/src/app/index.js CHANGED
@@ -1,15 +1,7 @@
1
- // What an app's host does for its page: the dialogs that pick files or show a message, opening an address or a file
2
- // outside the app, the app's folders, quitting, its menus, its tray icon and notifications, the events of the app's
3
- // native code and a desktop app's updates. Each call goes to the host of an app (target.runtimeEnv: 'app'); in any
4
- // other build it rejects, or events.on throws. The app's native code calls the same through <crossbind/app.h>, but for
5
- // the menus, the tray, notifications and events.on.
6
-
7
- const OPEN = { title: 'string', defaultPath: 'string', filters: 'filters', directory: 'boolean', multiple: 'boolean' };
8
- const SAVE = { title: 'string', defaultPath: 'string', defaultName: 'string', filters: 'filters' };
9
- const MESSAGE = { message: 'string', detail: 'string', kind: 'string', buttons: 'buttons' };
10
- const MESSAGE_KINDS = ['info', 'warning', 'error'];
11
- // Android's dialogs take three buttons at most.
12
- const MAX_BUTTONS = 3;
1
+ // What an app's host does for its page: the app's folders, quitting, its windows, the events of the app's native code and
2
+ // the functions of its extensions, by their names. Each call goes to the host of an app (target.runtimeEnv: 'app'); in any
3
+ // other build it rejects, or events.on throws. A page imports these from crossbind/app, which an app's bundler plugin
4
+ // takes from the app host, with what crossbind's own extensions give the page, as crossbind/app/core alone holds these.
13
5
 
14
6
  // The app's crossbind.js, which initNative() loads, hands the calls to the host.
15
7
  function hostOf(member, name) {
@@ -22,207 +14,148 @@ function hostOf(member, name) {
22
14
 
23
15
  const call = (name, options) => hostOf('app', name)(name, options);
24
16
 
25
- const isFilters = (value) => Array.isArray(value) && value.every((filter) => typeof filter?.name === 'string'
26
- && Array.isArray(filter.extensions) && filter.extensions.every((extension) => typeof extension === 'string'));
27
-
28
- // Spread reads a hole as undefined, which every() skips.
29
- const isButtons = (value) => Array.isArray(value) && value.length >= 1 && value.length <= MAX_BUTTONS
30
- && [...value].every((label) => typeof label === 'string' && label !== '');
31
-
32
- // The lists an option may be, beside what typeof tells: how each is checked, and what it is called.
33
- const LISTS = {
34
- filters: { isFitting: isFilters, name: 'list of { name, extensions }' },
35
- buttons: { isFitting: isButtons, name: 'list of one to three labels' },
36
- list: { isFitting: Array.isArray, name: 'list' },
37
- };
38
-
39
- // The options as the host takes them, so a misspelt or mistyped one fails here, saying which.
40
- function checked(name, options, kinds) {
17
+ // The options as the host takes them, so a misspelt or mistyped one fails here, saying which: each key's kind is what
18
+ // typeof gives, or { isFitting, name } for a list or an object, how it is checked and what it is called.
19
+ export function checkedOptions(name, options, kinds) {
41
20
  const keys = Object.keys(kinds).join(', ');
42
21
  if (options === null || typeof options !== 'object' || Array.isArray(options)) {
43
22
  throw new TypeError(`crossbind: ${name} takes an object of ${keys}.`);
44
23
  }
45
24
  for (const [key, value] of Object.entries(options)) {
46
25
  if (!Object.hasOwn(kinds, key)) throw new TypeError(`crossbind: ${name} takes ${keys}; ${key} is none of them.`);
47
- const list = LISTS[kinds[key]];
48
- if (value !== undefined && !(list ? list.isFitting(value) : typeof value === kinds[key])) {
49
- throw new TypeError(`crossbind: ${name}'s ${key} is no ${list ? list.name : kinds[key]}.`);
50
- }
51
- }
52
- return options;
53
- }
54
-
55
- function checkedMessage(options) {
56
- checked('dialog.message', options, MESSAGE);
57
- if (!options.message) throw new TypeError('crossbind: dialog.message takes a message.');
58
- if (options.kind !== undefined && !MESSAGE_KINDS.includes(options.kind)) {
59
- throw new TypeError(`crossbind: dialog.message's kind is info, warning or error, not ${JSON.stringify(options.kind)}.`);
26
+ const kind = kinds[key];
27
+ const isOfKind = typeof kind === 'string' ? typeof value === kind : kind.isFitting(value);
28
+ if (value !== undefined && !isOfKind) throw new TypeError(`crossbind: ${name}'s ${key} is no ${typeof kind === 'string' ? kind : kind.name}.`);
60
29
  }
61
30
  return options;
62
31
  }
63
32
 
64
- function checkedPath(name, path) {
65
- if (typeof path !== 'string' || !path) throw new TypeError(`crossbind: ${name} takes a path.`);
66
- return { path };
67
- }
68
-
69
- // A menu of the bar is { label, items } or a standard one by its role. An item is { label, id, accelerator, enabled,
70
- // checked }, a submenu { label, items }, { type: 'separator' }, or a standard item by its role, which a label may
71
- // rename and, but for an editing one, which keeps the platform's keys, an accelerator change.
72
- const MENU_ROLES = ['editMenu', 'windowMenu'];
73
- const EDITING_ROLES = ['undo', 'redo', 'cut', 'copy', 'paste', 'selectAll'];
74
- const ITEM_ROLES = [...EDITING_ROLES, 'minimize', 'close', 'quit'];
75
- const ITEM_KINDS = {
76
- label: 'string', id: 'string', accelerator: 'string', enabled: 'boolean', checked: 'boolean', items: 'list', role: 'string', type: 'string',
33
+ // A window's options, as windows.open() takes them, and those windows.set() changes: sizes and places in logical pixels.
34
+ // A window opens in the middle of the screen without x and y, and set() moves one there with center.
35
+ const WINDOW_SIZES = ['width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight'];
36
+ const WINDOW_SHARED = {
37
+ title: 'string', width: 'number', height: 'number', x: 'number', y: 'number', minWidth: 'number', minHeight: 'number',
38
+ maxWidth: 'number', maxHeight: 'number', resizable: 'boolean', alwaysOnTop: 'boolean', fullscreen: 'boolean',
77
39
  };
78
- const MODIFIERS = ['CmdOrCtrl', 'Ctrl', 'Alt', 'Shift'];
79
- const KEY = /^([A-Z0-9]|F([1-9]|1[0-2])|Enter|Escape|Tab|Space|Backspace|Delete|Up|Down|Left|Right|Home|End|PageUp|PageDown|Plus|Minus|Comma|Period)$/;
80
-
81
- // Typing uses neither a key with CmdOrCtrl, Ctrl or Alt nor a function key.
82
- function checkAccelerator(accelerator, where) {
83
- const modifiers = accelerator.split('+');
84
- const key = modifiers.pop();
85
- const isKnown = KEY.test(key) && modifiers.every((modifier) => MODIFIERS.includes(modifier)) && new Set(modifiers).size === modifiers.length;
86
- if (!isKnown || !(/^F\d+$/.test(key) || modifiers.some((modifier) => modifier !== 'Shift'))) {
87
- throw new TypeError(`crossbind: menu.set's ${where}.accelerator ${JSON.stringify(accelerator)} is none, which is a key with CmdOrCtrl, Ctrl or Alt and maybe Shift, joined by +, as CmdOrCtrl+Shift+O is, or F1 to F12, alone or with any of them.`);
40
+ const WINDOW_SET = { ...WINDOW_SHARED, center: 'boolean' };
41
+ const WINDOW_OPEN = {
42
+ ...WINDOW_SHARED, path: 'string', name: 'string', backgroundColor: 'string', frame: 'boolean', titleBar: 'string', parent: 'number', modal: 'boolean',
43
+ };
44
+ const TITLE_BARS = ['visible', 'hidden'];
45
+ const MAX_WINDOW_SIZE = 16384;
46
+ const MAX_WINDOW_PLACE = 1000000;
47
+ // The size a window opens at without one.
48
+ const WINDOW_SIZE = { width: 1024, height: 768 };
49
+ const COLOR = /^#[0-9a-f]{6}$/i;
50
+
51
+ const isWindowId = (id) => Number.isSafeInteger(id) && id >= 1;
52
+
53
+ function checkedWindow(name, options, kinds) {
54
+ checkedOptions(name, options, kinds);
55
+ const least = name === 'windows.set' ? 0 : 1;
56
+ for (const key of WINDOW_SIZES) {
57
+ const size = options[key];
58
+ // A limit of 0 that windows.set() gives takes it away.
59
+ const isFitting = Number.isInteger(size) && size >= ((key === 'width' || key === 'height') ? 1 : least) && size <= MAX_WINDOW_SIZE;
60
+ if (size !== undefined && !isFitting) throw new TypeError(`crossbind: ${name}'s ${key} is no whole number of pixels up to ${MAX_WINDOW_SIZE}.`);
88
61
  }
89
- }
90
-
91
- function checkMenuItem(item, where, isMenu) {
92
- if (item === null || typeof item !== 'object' || Array.isArray(item)) throw new TypeError(`crossbind: menu.set's ${where} is no object.`);
93
- for (const [key, value] of Object.entries(item)) {
94
- if (!Object.hasOwn(ITEM_KINDS, key)) throw new TypeError(`crossbind: menu.set's ${where} takes ${Object.keys(ITEM_KINDS).join(', ')}; ${key} is none of them.`);
95
- const kind = ITEM_KINDS[key];
96
- if (value !== undefined && !(kind === 'list' ? Array.isArray(value) : typeof value === kind)) {
97
- throw new TypeError(`crossbind: menu.set's ${where}.${key} is no ${kind}.`);
62
+ for (const key of ['x', 'y']) {
63
+ const place = options[key];
64
+ if (place !== undefined && !(Number.isInteger(place) && Math.abs(place) <= MAX_WINDOW_PLACE)) {
65
+ throw new TypeError(`crossbind: ${name}'s ${key} is no whole number of pixels from -${MAX_WINDOW_PLACE} to ${MAX_WINDOW_PLACE}.`);
98
66
  }
99
67
  }
100
- const has = (...keys) => keys.filter((key) => item[key] !== undefined);
101
- const refuse = (what) => {
102
- throw new TypeError(`crossbind: menu.set's ${where} ${what}.`);
103
- };
104
- if (item.type !== undefined) {
105
- if (item.type !== 'separator' || isMenu || has(...Object.keys(ITEM_KINDS)).length > 1) refuse("is no separator, which is { type: 'separator' } alone, inside a menu");
106
- } else if (item.role !== undefined) {
107
- const roles = isMenu ? MENU_ROLES : [...ITEM_ROLES, ...MENU_ROLES];
108
- if (!roles.includes(item.role)) refuse(`has no role ${JSON.stringify(item.role)}, which is one of ${roles.join(', ')}`);
109
- if (MENU_ROLES.includes(item.role) && has('id', 'accelerator', 'enabled', 'checked', 'items').length > 0) refuse('is a standard menu, which takes a label alone');
110
- if (EDITING_ROLES.includes(item.role) && item.accelerator !== undefined) refuse("is an editing item, which keeps the platform's keys and takes a label alone");
111
- if (has('id', 'enabled', 'checked', 'items').length > 0) refuse('has a role, which takes a label and an accelerator alone');
112
- } else if (!item.label) {
113
- refuse('has no label');
114
- } else if (item.items !== undefined) {
115
- if (has('id', 'accelerator', 'enabled', 'checked').length > 0) refuse('is a menu, which takes a label and items alone');
116
- // entries() reads a hole as undefined, which forEach skips.
117
- for (const [index, child] of item.items.entries()) checkMenuItem(child, `${where}.items[${index}]`, false);
118
- } else if (isMenu) {
119
- refuse('is a menu, which takes a label and items, or the role editMenu or windowMenu');
68
+ if ((options.x === undefined) !== (options.y === undefined)) throw new TypeError(`crossbind: ${name} takes x and y together.`);
69
+ const { path, backgroundColor, parent } = options;
70
+ if (path !== undefined && (!path.startsWith('/') || path.startsWith('//') || path.split(/[?#]/, 1)[0].split('/').some((part) => DOT_PART.test(part) || part.includes('\\')))) {
71
+ throw new TypeError(`crossbind: ${name}'s path ${JSON.stringify(path)} is no path of the app's own pages, as /settings.html is.`);
120
72
  }
121
- if (item.accelerator !== undefined) checkAccelerator(item.accelerator, where);
122
- }
123
-
124
- function checkedMenus(menus) {
125
- if (!Array.isArray(menus)) throw new TypeError('crossbind: menu.set takes a list of menus.');
126
- for (const [index, item] of menus.entries()) checkMenuItem(item, `menus[${index}]`, true);
127
- return { menus };
73
+ if (backgroundColor !== undefined && !COLOR.test(backgroundColor)) throw new TypeError(`crossbind: ${name}'s backgroundColor is no color of the form #rrggbb.`);
74
+ if (options.titleBar !== undefined && !TITLE_BARS.includes(options.titleBar)) throw new TypeError(`crossbind: ${name}'s titleBar is neither 'visible' nor 'hidden'.`);
75
+ if (options.frame === false && options.titleBar === 'hidden') {
76
+ throw new TypeError(`crossbind: ${name} hides the title bar of a window with a frame alone: drop titleBar, or frame: false.`);
77
+ }
78
+ if (parent !== undefined && !isWindowId(parent)) throw new TypeError(`crossbind: ${name}'s parent is no window's id.`);
79
+ if (options.modal && parent === undefined) throw new TypeError(`crossbind: ${name} opens a modal window above a parent: give it parent, the id of a window.`);
80
+ if (name === 'windows.open') checkedLimits(options);
81
+ return options;
128
82
  }
129
83
 
130
- // A tray item is { label, id, enabled, checked }, a submenu { label, items } or { type: 'separator' }: a tray's menu has
131
- // no keys, and no standard item.
132
- const TRAY = { tooltip: 'string', menu: 'list' };
133
- const TRAY_ITEM = { label: 'string', id: 'string', enabled: 'boolean', checked: 'boolean', items: 'list', type: 'string' };
134
- const NOTIFICATION = { title: 'string', body: 'string' };
135
-
136
- function checkTrayItem(item, where) {
137
- const refuse = (what) => {
138
- throw new TypeError(`crossbind: tray.set's ${where} ${what}.`);
139
- };
140
- if (item === null || typeof item !== 'object' || Array.isArray(item)) refuse('is no object');
141
- for (const [key, value] of Object.entries(item)) {
142
- if (!Object.hasOwn(TRAY_ITEM, key)) refuse(`takes ${Object.keys(TRAY_ITEM).join(', ')}; ${key} is none of them`);
143
- const kind = TRAY_ITEM[key];
144
- if (value !== undefined && !(kind === 'list' ? Array.isArray(value) : typeof value === kind)) refuse(`.${key} is no ${kind}`);
84
+ // The limits of a window that opens, as app.window takes them: those of one that resizes, around the size it opens at.
85
+ function checkedLimits(options) {
86
+ const limits = ['minWidth', 'minHeight', 'maxWidth', 'maxHeight'].filter((key) => options[key] !== undefined);
87
+ if (options.resizable === false && limits.length > 0) {
88
+ throw new TypeError(`crossbind: windows.open's ${limits.join(', ')} limit a window that resizes: drop them, or leave resizable true.`);
145
89
  }
146
- const others = Object.keys(item).filter((key) => key !== 'type' && item[key] !== undefined);
147
- if (item.type !== undefined) {
148
- if (item.type !== 'separator' || others.length > 0) refuse("is no separator, which is { type: 'separator' } alone");
149
- } else if (!item.label) {
150
- refuse('has no label');
151
- } else if (item.items !== undefined) {
152
- if (others.some((key) => key !== 'label' && key !== 'items')) refuse('is a menu, which takes a label and items alone');
153
- // entries() reads a hole as undefined, which forEach skips.
154
- for (const [index, child] of item.items.entries()) checkTrayItem(child, `${where}.items[${index}]`);
90
+ for (const [size, side] of [['width', 'Width'], ['height', 'Height']]) {
91
+ const opensAt = options[size] ?? WINDOW_SIZE[size];
92
+ if (options[`min${side}`] > opensAt) throw new TypeError(`crossbind: windows.open's min${side} is above the ${size} the window opens at, ${opensAt}.`);
93
+ if (options[`max${side}`] < opensAt) throw new TypeError(`crossbind: windows.open's max${side} is below the ${size} the window opens at, ${opensAt}.`);
155
94
  }
156
95
  }
157
96
 
158
- function checkedTray(options) {
159
- checked('tray.set', options, TRAY);
160
- for (const [index, item] of (options.menu ?? []).entries()) checkTrayItem(item, `menu[${index}]`);
161
- return options;
97
+ // The window a call names by its id, or with none the calling page's own.
98
+ function windowOf(name, id) {
99
+ if (id === undefined) return {};
100
+ if (!isWindowId(id)) throw new TypeError(`crossbind: ${name} takes the id of a window, as windows.open() gives it, or none for the page's own.`);
101
+ return { id };
162
102
  }
163
103
 
164
- function checkedNotification(options) {
165
- checked('notification.show', options, NOTIFICATION);
166
- if (!options.title) throw new TypeError('crossbind: notification.show takes a title.');
167
- return options;
104
+ // What the app's host tells the page of where the app runs, from before the page's own scripts on.
105
+ function deviceOf(name) {
106
+ const device = globalThis.__crossbind_device;
107
+ if (device) return device[name];
108
+ if (globalThis.__crossbind_token) throw new Error(`crossbind: this app's host is older than crossbind/app; update @crossbind/core-app-host to read device.${name}`);
109
+ throw new Error(`crossbind: device.${name} works in an app alone, whose crossbind.config.js sets target.runtimeEnv: 'app'`);
168
110
  }
169
111
 
170
- export const dialog = {
171
- // The paths of the files or folders the user picked, or null when they cancelled.
172
- open: async (options = {}) => call('dialog.open', checked('dialog.open', options, OPEN)),
173
- // The path the user chose to save to, or null when they cancelled.
174
- save: async (options = {}) => call('dialog.save', checked('dialog.save', options, SAVE)),
175
- // The index of the button the user clicked: Return gives the first, and Escape or closing the dialog the last.
176
- message: async (options) => call('dialog.message', checkedMessage(options)),
177
- };
178
-
179
- export const shell = {
180
- // An http, https or mailto address in the default browser or mail app.
181
- openExternal: async (url) => {
182
- if (typeof url !== 'string' && !(url instanceof URL)) throw new TypeError('crossbind: shell.openExternal takes an address.');
183
- return call('shell.openExternal', { url: String(url) });
184
- },
185
- // A file in the app the system opens its kind with. Opening a program runs it, and a script in the page could ask for
186
- // any path, so the host opens files where app.openPath in crossbind.config.js lets it alone; native code opens any.
187
- openPath: async (path) => call('shell.openPath', checkedPath('shell.openPath', path)),
188
- // A file, selected, in the system's file manager: one in the app's folders, or one a dialog picked during this run.
189
- revealInFolder: async (path) => call('shell.revealInFolder', checkedPath('shell.revealInFolder', path)),
190
- // A file or folder moved to the system's trash, where its user can take it back: one in the app's folders, not one of
191
- // them itself, or one a dialog picked during this run. Where the system has no trash, nothing moves.
192
- trash: async (path) => call('shell.trash', checkedPath('shell.trash', path)),
193
- };
112
+ // Where the app runs, the same through its run, which the page reads at once, as it starts: the platform and the arch
113
+ // the app is built for, what the device is to its user, its system's name and version, and whether a simulator or an
114
+ // emulator runs the app.
115
+ export const device = Object.freeze({
116
+ get platform() { return deviceOf('platform'); },
117
+ get arch() { return deviceOf('arch'); },
118
+ get formFactor() { return deviceOf('formFactor'); },
119
+ get os() { return deviceOf('os'); },
120
+ get isVirtual() { return deviceOf('isVirtual'); },
121
+ });
194
122
 
195
123
  export const app = {
196
124
  // The app's own folders, made when first asked for: { data, cache, logs }.
197
125
  paths: async () => call('app.paths', {}),
198
126
  // Ends the app as closing its window does.
199
127
  quit: async () => call('app.quit', {}),
128
+ // Asks the app's extensions, as its user's Quit does, then ends the app as quit() does, unless one keeps it running.
129
+ requestQuit: async () => call('app.requestQuit', {}),
200
130
  // Ends the app as quit() does, then opens it again, with no link.
201
131
  relaunch: async () => call('app.relaunch', {}),
202
- // The links of app.urlSchemes that opened the app and have not reached the page yet, the one it was launched with
203
- // among them; later ones come as the event open-url. Listening first, then asking, misses none.
204
- launchUrls: async () => call('app.launchUrls', {}),
205
- };
206
-
207
- export const menu = {
208
- // Sets the app's menus: on macOS after its app menu, which the app keeps, and on Windows and Linux in a bar of its
209
- // window. A click of an item with an id is an event, menu, of { id }, and of { id, checked } for an item it checked
210
- // or unchecked; a standard item does what its role says.
211
- set: async (menus) => call('menu.set', checkedMenus(menus)),
212
- };
213
-
214
- export const tray = {
215
- // The app's icon in the macOS menu bar, the Windows notification area or a Linux desktop's status tray, with its
216
- // tooltip and the menu a click opens. A click of an item with an id is the event menu, as one of menu.set's is, and a
217
- // click of the icon the event tray where it opens no menu. Set again, it changes the icon that shows.
218
- set: async (options = {}) => call('tray.set', checkedTray(options)),
219
- // Takes the icon away.
220
- remove: async () => call('tray.remove', {}),
132
+ // The launch that started a desktop app: { args, cwd }, its arguments without the program's own and without the links
133
+ // of the app's schemes, and the folder it was launched in. A phone's has neither.
134
+ launchArgs: async () => call('app.launchArgs', {}),
221
135
  };
222
136
 
223
- export const notification = {
224
- // A notification of the system's, with a title and a body; a click on it is the event notification.
225
- show: async (options) => call('notification.show', checkedNotification(options)),
137
+ export const windows = {
138
+ // Opens a window of the app's own page at path, '/' by default, in front: { id, ignored }, its id and the options the
139
+ // platform could not honour, which it opens without. A name already open brings that window to the front instead.
140
+ // The other functions name a window by its id, and with none the calling page's own.
141
+ open: async (options = {}) => call('windows.open', checkedWindow('windows.open', options, WINDOW_OPEN)),
142
+ // Changes an open window: { ignored }, as open() gives it.
143
+ set: async (id, options) => {
144
+ const [named, changes] = typeof id === 'object' && options === undefined ? [undefined, id] : [id, options];
145
+ return call('windows.set', { ...checkedWindow('windows.set', changes, WINDOW_SET), ...windowOf('windows.set', named) });
146
+ },
147
+ // Closes a window as its user does, which an extension may keep open.
148
+ close: async (id) => call('windows.close', windowOf('windows.close', id)),
149
+ // Brings a window to the front, restored if it was minimized.
150
+ focus: async (id) => call('windows.focus', windowOf('windows.focus', id)),
151
+ minimize: async (id) => call('windows.minimize', windowOf('windows.minimize', id)),
152
+ toggleMaximize: async (id) => call('windows.toggleMaximize', windowOf('windows.toggleMaximize', id)),
153
+ // Moves the page's window with the pointer, as its title bar does, from a mousedown of the primary button.
154
+ startDrag: async () => call('windows.startDrag', {}),
155
+ // The open windows, the oldest first, and the page's own: { id, name, title, x, y, width, height, isMaximized,
156
+ // isMinimized, isFullscreen, isFocused }.
157
+ list: async () => call('windows.list', {}),
158
+ current: async () => call('windows.current', {}),
226
159
  };
227
160
 
228
161
  export const events = {
@@ -233,13 +166,42 @@ export const events = {
233
166
  if (typeof listener !== 'function') throw new TypeError('crossbind: events.on takes a function to call.');
234
167
  return hostOf('on', 'events.on')(name, listener);
235
168
  },
169
+ // The values of the events of this name the app's native code held for the page with holdEvent(), which came before
170
+ // the page asked for them since it loaded; later ones come as the events while it listens. Listening first, then
171
+ // asking, misses none.
172
+ held: async (name) => {
173
+ if (typeof name !== 'string' || !name) throw new TypeError('crossbind: events.held takes the name of an event.');
174
+ return call('events.held', { name });
175
+ },
176
+ };
177
+
178
+ // A function of one of the app's extensions, by its name, as clipboard.readText, with options of JSON values and bytes,
179
+ // a typed array, a DataView or an ArrayBuffer, whose result's bytes come back as a Uint8Array; an extension's own
180
+ // package calls it so. The host checks the options. onMessage gets each message the function sends before it answers,
181
+ // in order, all of them before invoke resolves; signal cancels the call, which then rejects with the signal's reason.
182
+ export const invoke = async (name, options = {}, { onMessage, signal } = {}) => {
183
+ if (typeof name !== 'string' || !name) throw new TypeError("crossbind: invoke takes the name of one of the app's functions.");
184
+ if (onMessage !== undefined && typeof onMessage !== 'function') throw new TypeError('crossbind: invoke takes onMessage as a function.');
185
+ if (signal !== undefined && !(signal instanceof AbortSignal)) throw new TypeError('crossbind: invoke takes signal as an AbortSignal.');
186
+ const host = hostOf('app', name);
187
+ // An older host would leave them out without a word.
188
+ if (onMessage !== undefined || signal !== undefined) hostOf('takesMessages', 'invoke with onMessage or signal');
189
+ return host(name, options, { onMessage, signal });
236
190
  };
237
191
 
238
- // One check or install runs at a time, and another rejects meanwhile.
239
- export const updater = {
240
- // The version app.updater.url in crossbind.config.js offers when it is newer than the app's, as { version }, or null.
241
- check: async () => call('updater.check', {}),
242
- // Downloads that version, checks that the key of app.updater.publicKey signed it and installs it in place of the app,
243
- // which then quits and opens the new version. It rejects when that fails.
244
- install: async () => call('updater.install', {}),
192
+ const EXTENSION_NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
193
+ // A part of a path the URL would resolve away, escaped or not, to another extension's or the page's own.
194
+ const DOT_PART = /^(\.|%2e){1,2}$/i;
195
+ const ANDROID_ORIGIN = 'https://appassets.androidplatform.net';
196
+
197
+ // The URL of a path an extension's route serves, as "/clips/intro.mp4" or "/thumb/42?size=256", for the page's <img>,
198
+ // <video> or fetch(), as the URL holds it: what a path takes escaped, the caller escapes. The route gets it decoded.
199
+ // The app's own origin is an https one on Android, whose WebView serves no scheme of an app's.
200
+ export const routeUrl = (extension, path) => {
201
+ if (typeof extension !== 'string' || !EXTENSION_NAME.test(extension)) throw new TypeError("crossbind: routeUrl takes the name of one of the app's extensions.");
202
+ if (typeof path !== 'string' || !path.startsWith('/')) throw new TypeError('crossbind: routeUrl takes a path that starts with "/".');
203
+ if (path.split(/[?#]/, 1)[0].split('/').some((part) => DOT_PART.test(part))) throw new TypeError('crossbind: routeUrl takes a path without a part "." or "..".');
204
+ const isAndroid = Boolean(globalThis.__crossbind_port) || globalThis.location?.origin === ANDROID_ORIGIN;
205
+ return `${isAndroid ? ANDROID_ORIGIN : 'crossbind://app'}/ext/${extension}${path}`;
245
206
  };
207
+
File without changes
@@ -5,7 +5,6 @@ import { registerHooks } from 'node:module';
5
5
  import path from 'node:path';
6
6
  import { fileURLToPath, pathToFileURL } from 'node:url';
7
7
 
8
- const SCHEME = /^(cargo|conan):/;
9
8
  const isBare = (specifier) => !/^(\.{1,2}\/|\/|[A-Za-z]:[\\/]|file:)/.test(specifier);
10
9
 
11
10
  function notBound(what, parentURL, cause) {
@@ -17,23 +16,25 @@ const exportOf = (name) => (typeof name === 'string' ? name : `${name.wire} as $
17
16
  const proxyOf = (entry, names) => `export { ${['initNative', 'AllSymbols', ...names.map(exportOf)].join(', ')} } from ${JSON.stringify(entry)};\n`;
18
17
 
19
18
  // Another hook may still serve it; Node's own resolver hands an unknown scheme on unresolved.
20
- function resolveUnbound(specifier, context, nextResolve) {
19
+ function resolveUnbound(specifier, context, nextResolve, scheme) {
21
20
  let resolved;
22
21
  try {
23
22
  resolved = nextResolve(specifier, context);
24
23
  } catch (error) {
25
24
  throw notBound(specifier, context.parentURL, error);
26
25
  }
27
- if (SCHEME.test(resolved.url)) throw notBound(specifier, context.parentURL);
26
+ if (scheme.test(resolved.url)) throw notBound(specifier, context.parentURL);
28
27
  return resolved;
29
28
  }
30
29
 
31
30
  // `natives` lists each bound file, relative to the entry, with the bare specifiers that name it and the names it
32
- // exports, a name the entry exports under another as { local, wire }.
33
- export default function registerNativeImports(entry, { extensions, natives }) {
31
+ // exports, a name the entry exports under another as { local, wire }. `schemes` are those an import names a store
32
+ // by: the two every build has, for a register file an older build wrote, which names none.
33
+ export default function registerNativeImports(entry, { extensions, schemes = ['cargo', 'conan'], natives }) {
34
34
  const entryUrl = new URL(entry).href;
35
35
  const dir = path.dirname(fileURLToPath(entryUrl));
36
36
  const isNative = new RegExp(`\\.(${extensions.join('|')})$`);
37
+ const scheme = new RegExp(`^(${schemes.join('|')}):`);
37
38
  const byFile = new Map();
38
39
  const bySpecifier = new Map();
39
40
  natives.forEach(({ file, specifiers, names }) => {
@@ -46,7 +47,7 @@ export default function registerNativeImports(entry, { extensions, natives }) {
46
47
  resolve(specifier, context, nextResolve) {
47
48
  const url = bySpecifier.get(specifier);
48
49
  if (url) return { url, format: 'module', shortCircuit: true };
49
- if (isBare(specifier) && (SCHEME.test(specifier) || isNative.test(specifier))) return resolveUnbound(specifier, context, nextResolve);
50
+ if (isBare(specifier) && (scheme.test(specifier) || isNative.test(specifier))) return resolveUnbound(specifier, context, nextResolve, scheme);
50
51
  return nextResolve(specifier, context);
51
52
  },
52
53
  load(url, context, nextLoad) {
@@ -1,55 +1,55 @@
1
1
  {
2
- "version": "1.0.18",
2
+ "version": "1.0.20",
3
3
  "registry": "ghcr.io/crossbind",
4
4
  "toolchains": {
5
5
  "rust": "1.99.1"
6
6
  },
7
7
  "images": {
8
8
  "rust-sysroot": {
9
- "index": "sha256:31a213289c4dc1125b462ef006cc27b84ee7eca4e3ef0a93259bd698d56c7359",
9
+ "index": "sha256:3f62871ccbebbdca53b798fa1b21173bee848ada7744f5108df66a290d3cca78",
10
10
  "platforms": {
11
- "linux/arm64": "sha256:e82b9a4b9661356238b264a9d5b6921d364cbc81c54ec8426349527144b91bbc",
12
- "linux/amd64": "sha256:33c63cb3ed60a6d771f079a60c97dfa1f183f215d4087bb655ff4deca2366d9a"
11
+ "linux/arm64": "sha256:ec7c2bba3add2385099b71520c5d68540e8dba691f57a42c0e9143ea5b8981df",
12
+ "linux/amd64": "sha256:dfad6e04d3168f5e7375aba271d0e615cc6658793527503ce3070efbd4ac5f91"
13
13
  }
14
14
  },
15
15
  "base": {
16
- "index": "sha256:ca5b17d648e797193d7f3c67635e1e73e56b58d02699a55ea61a35eb5ca8d0c8",
16
+ "index": "sha256:a5c7ea17f3e13c13d3c9f53307cce95d037bbb6b58b08034cfa2cb8112a63407",
17
17
  "platforms": {
18
- "linux/arm64": "sha256:48666c55a2f943d8be029080fab985839bc50231b8556a00100bc3702a7bcc51",
19
- "linux/amd64": "sha256:847f3c686dc8a846b05ee76d38a79b9ac68fb293ca014d56967ee12859849f1f"
18
+ "linux/amd64": "sha256:a0934c868ccd5a09de14d193ae33b091ecfddf3c13548595e918c0b053e41dd6",
19
+ "linux/arm64": "sha256:14d4bcea7537972052cd6cee314a5c08f1cd68eebd3a7461ff7ca594994c49eb"
20
20
  }
21
21
  },
22
22
  "web": {
23
- "index": "sha256:72cdc6f5b41d946edc7ca8cd5e1bcf7314f03dfc71be9067994b38130f965711",
23
+ "index": "sha256:00a19f61fa46315175be11f20cea051311c218ab6e99bd1bca70236415a69af9",
24
24
  "platforms": {
25
- "linux/arm64": "sha256:6d1756fa994e5d939ac5696ebe32012f56c95d0428a8bc7b3775d53acbff8735",
26
- "linux/amd64": "sha256:14aa215c2157e485ce6d664e0077a3cf6938733855b8146e97830e48fda13875"
25
+ "linux/arm64": "sha256:6e75223e8c696d93f5ab57e286425b4b94c4b1dcb3cf291e63c2820636dfa2a6",
26
+ "linux/amd64": "sha256:fe4e84f435838a35f2d60774be0050c39f2ecf1b8ed6cca23530b8c32f5c6211"
27
27
  }
28
28
  },
29
29
  "android": {
30
- "index": "sha256:7cbd98594a1fb1f48fe156a21c6c5256cd535d2ffd152c1f80f2860d4dc5b6f0",
30
+ "index": "sha256:df5bf439ab81afbe28a166dcd3c0ec281aea11d4265b1fc24225cc686f643ce1",
31
31
  "platforms": {
32
- "linux/amd64": "sha256:a6e99a0a66e038fd0e6d1154df6c7c58d68acd9def896b63f542d810d923bd87"
32
+ "linux/amd64": "sha256:d2f5460a998aa1695b04d0d1bc231af4d77dd78931ca0f2b02adec51a61f0c02"
33
33
  }
34
34
  },
35
35
  "android-app": {
36
- "index": "sha256:5cfd1df40ae28730d592f76ade7e08e628f0002d73979015308d50727f021f3e",
36
+ "index": "sha256:d72766f41a7d0343fdf36a425a9c5e0ea7dda8ac6f3b2697fe49d79a21e5018b",
37
37
  "platforms": {
38
- "linux/amd64": "sha256:98d035dfaeb27095d0a4edce83299ebbb3ffa8e360a4cb925db7cd0b0b8a4666"
38
+ "linux/amd64": "sha256:8e62eea8685cd28276dfdd28b34aa75b3a0a677b40a93ea3f28f7f3ef1565425"
39
39
  }
40
40
  },
41
41
  "linux": {
42
- "index": "sha256:9114bdc0e6b2c1b753d2ba154181566c320e9a901030a14c707f174b7e4849f5",
42
+ "index": "sha256:a4828cf2821cc2313f5ddf5bc761073c90616fbef09daa8b8d93e7fa3050497a",
43
43
  "platforms": {
44
- "linux/arm64": "sha256:47e104db909dc78a995733d3d27db94e135f1044e1f0860bb201930a65273af0",
45
- "linux/amd64": "sha256:cd13e6b3f5c5303a583da5267531a83839fe8cf7979890463fc45a914a6d5d3f"
44
+ "linux/arm64": "sha256:501a878b581295d3946945dfa9bc98a0aa3de5d4c6bc006c7224756cccbf1e35",
45
+ "linux/amd64": "sha256:ee5a4f7ee23fdda7e50d4130d783a76d7c44d079ae34b4a326565510eea761dc"
46
46
  }
47
47
  },
48
48
  "windows": {
49
- "index": "sha256:46e6da18313f4fd3b599fb2505383aa34f32ffe7ceb659426fa3fb71e6f1e8e7",
49
+ "index": "sha256:13fc02ab67d1465cbe75715d7de6a87c5319477eba2c0459c914bb5b698df766",
50
50
  "platforms": {
51
- "linux/arm64": "sha256:b6457367be5c6863f8d336ce426b9dba7fdfa192308876e4bc390d82a6e18474",
52
- "linux/amd64": "sha256:fd5bdc1f34d64bb9aebae1e1d44fc380c1dd815bd6686349e428d4551732f6e0"
51
+ "linux/amd64": "sha256:78883c59167af69c5b9433ef6cdfa35aab3833501bb85d830d95f24079a6c8d0",
52
+ "linux/arm64": "sha256:f27f09f8e661261c770976580d9861d4ce290eb1ac3030e6198f3c0a4987bd8a"
53
53
  }
54
54
  }
55
55
  }