crossbind 2.0.0-beta.68 → 2.0.0-beta.69
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 +1 -1
- package/package.json +69 -59
- package/src/actions/addExtension.js +191 -0
- package/src/actions/androidGradle.js +26 -17
- package/src/actions/appInstaller.js +69 -153
- package/src/actions/appSwift.js +66 -0
- package/src/actions/buildApp.js +172 -74
- package/src/actions/buildNode.js +6 -1
- package/src/actions/buildWasm.js +0 -9
- package/src/actions/createInterface.js +17 -8
- package/src/actions/createLib.js +0 -4
- package/src/actions/extensionApi.js +298 -0
- package/src/actions/extensionCommands.js +41 -0
- package/src/actions/extensionsInApp.js +19 -0
- package/src/actions/gradleProject.js +196 -16
- package/src/actions/iosDevice.js +2 -2
- package/src/actions/iosSimulator.js +3 -3
- package/src/actions/licenses.js +83 -34
- package/src/actions/linuxPackages.js +48 -0
- package/src/actions/mavenVerification.js +37 -0
- package/src/actions/publish.js +220 -0
- package/src/actions/windowsSigning.js +1 -1
- package/src/actions/xcodeProject.js +49 -24
- package/src/actions/xcodeTargets.js +122 -0
- package/src/actions/xcodebuild.js +65 -0
- package/src/app/index.d.ts +126 -133
- package/src/app/index.js +126 -188
- package/src/assets/configure/install.sh +0 -0
- package/src/assets/toolchain-digests.json +20 -20
- package/src/bin.js +20 -28
- package/src/index.js +2 -1
- package/src/program.js +32 -3
- package/src/state/index.js +2 -1
- package/src/state/loadConfig.js +16 -5
- package/src/utils/appExtensions.js +761 -0
- package/src/utils/appFiles.js +58 -0
- package/src/utils/appLaunches.js +249 -0
- package/src/utils/appOptions.js +109 -100
- package/src/utils/appTargets.js +168 -0
- package/src/utils/bridgeTools.js +66 -0
- package/src/utils/configChanges.js +30 -0
- package/src/utils/configWriter.js +143 -26
- package/src/utils/extensionSteps.js +135 -0
- package/src/utils/openUrl.js +2 -1
- package/src/utils/plist.js +5 -0
- package/src/utils/resolveAppHost.js +7 -0
- package/src/utils/systemConfig.js +16 -3
- package/vitest.config.js +6 -1
- package/src/actions/appSetup.js +0 -116
- package/src/actions/appStoreConnect.js +0 -188
- package/src/actions/appUpdate.js +0 -68
- package/src/actions/extensions.js +0 -7
- package/src/actions/googlePlay.js +0 -213
- package/src/actions/googleSignIn.js +0 -78
- package/src/actions/playSetup.js +0 -19
- package/src/actions/publishAndroid.js +0 -102
- package/src/actions/publishIos.js +0 -146
package/src/app/index.js
CHANGED
|
@@ -1,15 +1,7 @@
|
|
|
1
|
-
// What an app's host does for its page: the
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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,124 @@ function hostOf(member, name) {
|
|
|
22
14
|
|
|
23
15
|
const call = (name, options) => hostOf('app', name)(name, options);
|
|
24
16
|
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
40
|
+
const WINDOW_SET = { ...WINDOW_SHARED, center: 'boolean' };
|
|
41
|
+
const WINDOW_OPEN = {
|
|
42
|
+
...WINDOW_SHARED, path: 'string', name: 'string', backgroundColor: 'string', frame: 'boolean', parent: 'number', modal: 'boolean',
|
|
43
|
+
};
|
|
44
|
+
const MAX_WINDOW_SIZE = 16384;
|
|
45
|
+
const MAX_WINDOW_PLACE = 1000000;
|
|
46
|
+
// The size a window opens at without one.
|
|
47
|
+
const WINDOW_SIZE = { width: 1024, height: 768 };
|
|
48
|
+
const COLOR = /^#[0-9a-f]{6}$/i;
|
|
49
|
+
|
|
50
|
+
const isWindowId = (id) => Number.isSafeInteger(id) && id >= 1;
|
|
51
|
+
|
|
52
|
+
function checkedWindow(name, options, kinds) {
|
|
53
|
+
checkedOptions(name, options, kinds);
|
|
54
|
+
const least = name === 'windows.set' ? 0 : 1;
|
|
55
|
+
for (const key of WINDOW_SIZES) {
|
|
56
|
+
const size = options[key];
|
|
57
|
+
// A limit of 0 that windows.set() gives takes it away.
|
|
58
|
+
const isFitting = Number.isInteger(size) && size >= ((key === 'width' || key === 'height') ? 1 : least) && size <= MAX_WINDOW_SIZE;
|
|
59
|
+
if (size !== undefined && !isFitting) throw new TypeError(`crossbind: ${name}'s ${key} is no whole number of pixels up to ${MAX_WINDOW_SIZE}.`);
|
|
88
60
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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}.`);
|
|
61
|
+
for (const key of ['x', 'y']) {
|
|
62
|
+
const place = options[key];
|
|
63
|
+
if (place !== undefined && !(Number.isInteger(place) && Math.abs(place) <= MAX_WINDOW_PLACE)) {
|
|
64
|
+
throw new TypeError(`crossbind: ${name}'s ${key} is no whole number of pixels from -${MAX_WINDOW_PLACE} to ${MAX_WINDOW_PLACE}.`);
|
|
98
65
|
}
|
|
99
66
|
}
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
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');
|
|
67
|
+
if ((options.x === undefined) !== (options.y === undefined)) throw new TypeError(`crossbind: ${name} takes x and y together.`);
|
|
68
|
+
const { path, backgroundColor, parent } = options;
|
|
69
|
+
if (path !== undefined && (!path.startsWith('/') || path.startsWith('//') || path.split(/[?#]/, 1)[0].split('/').some((part) => DOT_PART.test(part) || part.includes('\\')))) {
|
|
70
|
+
throw new TypeError(`crossbind: ${name}'s path ${JSON.stringify(path)} is no path of the app's own pages, as /settings.html is.`);
|
|
120
71
|
}
|
|
121
|
-
if (
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
for (const [index, item] of menus.entries()) checkMenuItem(item, `menus[${index}]`, true);
|
|
127
|
-
return { menus };
|
|
72
|
+
if (backgroundColor !== undefined && !COLOR.test(backgroundColor)) throw new TypeError(`crossbind: ${name}'s backgroundColor is no color of the form #rrggbb.`);
|
|
73
|
+
if (parent !== undefined && !isWindowId(parent)) throw new TypeError(`crossbind: ${name}'s parent is no window's id.`);
|
|
74
|
+
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.`);
|
|
75
|
+
if (name === 'windows.open') checkedLimits(options);
|
|
76
|
+
return options;
|
|
128
77
|
}
|
|
129
78
|
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
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}`);
|
|
79
|
+
// The limits of a window that opens, as app.window takes them: those of one that resizes, around the size it opens at.
|
|
80
|
+
function checkedLimits(options) {
|
|
81
|
+
const limits = ['minWidth', 'minHeight', 'maxWidth', 'maxHeight'].filter((key) => options[key] !== undefined);
|
|
82
|
+
if (options.resizable === false && limits.length > 0) {
|
|
83
|
+
throw new TypeError(`crossbind: windows.open's ${limits.join(', ')} limit a window that resizes: drop them, or leave resizable true.`);
|
|
145
84
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
if (
|
|
149
|
-
|
|
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}]`);
|
|
85
|
+
for (const [size, side] of [['width', 'Width'], ['height', 'Height']]) {
|
|
86
|
+
const opensAt = options[size] ?? WINDOW_SIZE[size];
|
|
87
|
+
if (options[`min${side}`] > opensAt) throw new TypeError(`crossbind: windows.open's min${side} is above the ${size} the window opens at, ${opensAt}.`);
|
|
88
|
+
if (options[`max${side}`] < opensAt) throw new TypeError(`crossbind: windows.open's max${side} is below the ${size} the window opens at, ${opensAt}.`);
|
|
155
89
|
}
|
|
156
90
|
}
|
|
157
91
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
|
|
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;
|
|
92
|
+
// The window a call names by its id, or with none the calling page's own.
|
|
93
|
+
function windowOf(name, id) {
|
|
94
|
+
if (id === undefined) return {};
|
|
95
|
+
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.`);
|
|
96
|
+
return { id };
|
|
168
97
|
}
|
|
169
98
|
|
|
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
|
-
};
|
|
194
|
-
|
|
195
99
|
export const app = {
|
|
196
100
|
// The app's own folders, made when first asked for: { data, cache, logs }.
|
|
197
101
|
paths: async () => call('app.paths', {}),
|
|
198
102
|
// Ends the app as closing its window does.
|
|
199
103
|
quit: async () => call('app.quit', {}),
|
|
104
|
+
// Asks the app's extensions, as its user's Quit does, then ends the app as quit() does, unless one keeps it running.
|
|
105
|
+
requestQuit: async () => call('app.requestQuit', {}),
|
|
200
106
|
// Ends the app as quit() does, then opens it again, with no link.
|
|
201
107
|
relaunch: async () => call('app.relaunch', {}),
|
|
202
|
-
// The
|
|
203
|
-
//
|
|
204
|
-
|
|
108
|
+
// The launch that started a desktop app: { args, cwd }, its arguments without the program's own and without the links
|
|
109
|
+
// of the app's schemes, and the folder it was launched in. A phone's has neither.
|
|
110
|
+
launchArgs: async () => call('app.launchArgs', {}),
|
|
205
111
|
};
|
|
206
112
|
|
|
207
|
-
export const
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
//
|
|
218
|
-
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
113
|
+
export const windows = {
|
|
114
|
+
// Opens a window of the app's own page at path, '/' by default, in front: { id, ignored }, its id and the options the
|
|
115
|
+
// platform could not honour, which it opens without. A name already open brings that window to the front instead.
|
|
116
|
+
// The other functions name a window by its id, and with none the calling page's own.
|
|
117
|
+
open: async (options = {}) => call('windows.open', checkedWindow('windows.open', options, WINDOW_OPEN)),
|
|
118
|
+
// Changes an open window: { ignored }, as open() gives it.
|
|
119
|
+
set: async (id, options) => {
|
|
120
|
+
const [named, changes] = typeof id === 'object' && options === undefined ? [undefined, id] : [id, options];
|
|
121
|
+
return call('windows.set', { ...checkedWindow('windows.set', changes, WINDOW_SET), ...windowOf('windows.set', named) });
|
|
122
|
+
},
|
|
123
|
+
// Closes a window as its user does, which an extension may keep open.
|
|
124
|
+
close: async (id) => call('windows.close', windowOf('windows.close', id)),
|
|
125
|
+
// Brings a window to the front, restored if it was minimized.
|
|
126
|
+
focus: async (id) => call('windows.focus', windowOf('windows.focus', id)),
|
|
127
|
+
minimize: async (id) => call('windows.minimize', windowOf('windows.minimize', id)),
|
|
128
|
+
toggleMaximize: async (id) => call('windows.toggleMaximize', windowOf('windows.toggleMaximize', id)),
|
|
129
|
+
// Moves the page's window with the pointer, as its title bar does, from a mousedown of the primary button.
|
|
130
|
+
startDrag: async () => call('windows.startDrag', {}),
|
|
131
|
+
// The open windows, the oldest first, and the page's own: { id, name, title, x, y, width, height, isMaximized,
|
|
132
|
+
// isMinimized, isFullscreen, isFocused }.
|
|
133
|
+
list: async () => call('windows.list', {}),
|
|
134
|
+
current: async () => call('windows.current', {}),
|
|
226
135
|
};
|
|
227
136
|
|
|
228
137
|
export const events = {
|
|
@@ -233,13 +142,42 @@ export const events = {
|
|
|
233
142
|
if (typeof listener !== 'function') throw new TypeError('crossbind: events.on takes a function to call.');
|
|
234
143
|
return hostOf('on', 'events.on')(name, listener);
|
|
235
144
|
},
|
|
145
|
+
// The values of the events of this name the app's native code held for the page with holdEvent(), which came before
|
|
146
|
+
// the page asked for them since it loaded; later ones come as the events while it listens. Listening first, then
|
|
147
|
+
// asking, misses none.
|
|
148
|
+
held: async (name) => {
|
|
149
|
+
if (typeof name !== 'string' || !name) throw new TypeError('crossbind: events.held takes the name of an event.');
|
|
150
|
+
return call('events.held', { name });
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// A function of one of the app's extensions, by its name, as clipboard.readText, with options of JSON values and bytes,
|
|
155
|
+
// a typed array, a DataView or an ArrayBuffer, whose result's bytes come back as a Uint8Array; an extension's own
|
|
156
|
+
// package calls it so. The host checks the options. onMessage gets each message the function sends before it answers,
|
|
157
|
+
// in order, all of them before invoke resolves; signal cancels the call, which then rejects with the signal's reason.
|
|
158
|
+
export const invoke = async (name, options = {}, { onMessage, signal } = {}) => {
|
|
159
|
+
if (typeof name !== 'string' || !name) throw new TypeError("crossbind: invoke takes the name of one of the app's functions.");
|
|
160
|
+
if (onMessage !== undefined && typeof onMessage !== 'function') throw new TypeError('crossbind: invoke takes onMessage as a function.');
|
|
161
|
+
if (signal !== undefined && !(signal instanceof AbortSignal)) throw new TypeError('crossbind: invoke takes signal as an AbortSignal.');
|
|
162
|
+
const host = hostOf('app', name);
|
|
163
|
+
// An older host would leave them out without a word.
|
|
164
|
+
if (onMessage !== undefined || signal !== undefined) hostOf('takesMessages', 'invoke with onMessage or signal');
|
|
165
|
+
return host(name, options, { onMessage, signal });
|
|
236
166
|
};
|
|
237
167
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
168
|
+
const EXTENSION_NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
169
|
+
// A part of a path the URL would resolve away, escaped or not, to another extension's or the page's own.
|
|
170
|
+
const DOT_PART = /^(\.|%2e){1,2}$/i;
|
|
171
|
+
const ANDROID_ORIGIN = 'https://appassets.androidplatform.net';
|
|
172
|
+
|
|
173
|
+
// The URL of a path an extension's route serves, as "/clips/intro.mp4" or "/thumb/42?size=256", for the page's <img>,
|
|
174
|
+
// <video> or fetch(), as the URL holds it: what a path takes escaped, the caller escapes. The route gets it decoded.
|
|
175
|
+
// The app's own origin is an https one on Android, whose WebView serves no scheme of an app's.
|
|
176
|
+
export const routeUrl = (extension, path) => {
|
|
177
|
+
if (typeof extension !== 'string' || !EXTENSION_NAME.test(extension)) throw new TypeError("crossbind: routeUrl takes the name of one of the app's extensions.");
|
|
178
|
+
if (typeof path !== 'string' || !path.startsWith('/')) throw new TypeError('crossbind: routeUrl takes a path that starts with "/".');
|
|
179
|
+
if (path.split(/[?#]/, 1)[0].split('/').some((part) => DOT_PART.test(part))) throw new TypeError('crossbind: routeUrl takes a path without a part "." or "..".');
|
|
180
|
+
const isAndroid = Boolean(globalThis.__crossbind_port) || globalThis.location?.origin === ANDROID_ORIGIN;
|
|
181
|
+
return `${isAndroid ? ANDROID_ORIGIN : 'crossbind://app'}/ext/${extension}${path}`;
|
|
245
182
|
};
|
|
183
|
+
|
|
File without changes
|
|
@@ -1,55 +1,55 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.0.
|
|
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:
|
|
9
|
+
"index": "sha256:3f62871ccbebbdca53b798fa1b21173bee848ada7744f5108df66a290d3cca78",
|
|
10
10
|
"platforms": {
|
|
11
|
-
"linux/arm64": "sha256:
|
|
12
|
-
"linux/amd64": "sha256:
|
|
11
|
+
"linux/arm64": "sha256:ec7c2bba3add2385099b71520c5d68540e8dba691f57a42c0e9143ea5b8981df",
|
|
12
|
+
"linux/amd64": "sha256:dfad6e04d3168f5e7375aba271d0e615cc6658793527503ce3070efbd4ac5f91"
|
|
13
13
|
}
|
|
14
14
|
},
|
|
15
15
|
"base": {
|
|
16
|
-
"index": "sha256:
|
|
16
|
+
"index": "sha256:a5c7ea17f3e13c13d3c9f53307cce95d037bbb6b58b08034cfa2cb8112a63407",
|
|
17
17
|
"platforms": {
|
|
18
|
-
"linux/
|
|
19
|
-
"linux/
|
|
18
|
+
"linux/amd64": "sha256:a0934c868ccd5a09de14d193ae33b091ecfddf3c13548595e918c0b053e41dd6",
|
|
19
|
+
"linux/arm64": "sha256:14d4bcea7537972052cd6cee314a5c08f1cd68eebd3a7461ff7ca594994c49eb"
|
|
20
20
|
}
|
|
21
21
|
},
|
|
22
22
|
"web": {
|
|
23
|
-
"index": "sha256:
|
|
23
|
+
"index": "sha256:00a19f61fa46315175be11f20cea051311c218ab6e99bd1bca70236415a69af9",
|
|
24
24
|
"platforms": {
|
|
25
|
-
"linux/arm64": "sha256:
|
|
26
|
-
"linux/amd64": "sha256:
|
|
25
|
+
"linux/arm64": "sha256:6e75223e8c696d93f5ab57e286425b4b94c4b1dcb3cf291e63c2820636dfa2a6",
|
|
26
|
+
"linux/amd64": "sha256:fe4e84f435838a35f2d60774be0050c39f2ecf1b8ed6cca23530b8c32f5c6211"
|
|
27
27
|
}
|
|
28
28
|
},
|
|
29
29
|
"android": {
|
|
30
|
-
"index": "sha256:
|
|
30
|
+
"index": "sha256:df5bf439ab81afbe28a166dcd3c0ec281aea11d4265b1fc24225cc686f643ce1",
|
|
31
31
|
"platforms": {
|
|
32
|
-
"linux/amd64": "sha256:
|
|
32
|
+
"linux/amd64": "sha256:d2f5460a998aa1695b04d0d1bc231af4d77dd78931ca0f2b02adec51a61f0c02"
|
|
33
33
|
}
|
|
34
34
|
},
|
|
35
35
|
"android-app": {
|
|
36
|
-
"index": "sha256:
|
|
36
|
+
"index": "sha256:d72766f41a7d0343fdf36a425a9c5e0ea7dda8ac6f3b2697fe49d79a21e5018b",
|
|
37
37
|
"platforms": {
|
|
38
|
-
"linux/amd64": "sha256:
|
|
38
|
+
"linux/amd64": "sha256:8e62eea8685cd28276dfdd28b34aa75b3a0a677b40a93ea3f28f7f3ef1565425"
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
"linux": {
|
|
42
|
-
"index": "sha256:
|
|
42
|
+
"index": "sha256:a4828cf2821cc2313f5ddf5bc761073c90616fbef09daa8b8d93e7fa3050497a",
|
|
43
43
|
"platforms": {
|
|
44
|
-
"linux/arm64": "sha256:
|
|
45
|
-
"linux/amd64": "sha256:
|
|
44
|
+
"linux/arm64": "sha256:501a878b581295d3946945dfa9bc98a0aa3de5d4c6bc006c7224756cccbf1e35",
|
|
45
|
+
"linux/amd64": "sha256:ee5a4f7ee23fdda7e50d4130d783a76d7c44d079ae34b4a326565510eea761dc"
|
|
46
46
|
}
|
|
47
47
|
},
|
|
48
48
|
"windows": {
|
|
49
|
-
"index": "sha256:
|
|
49
|
+
"index": "sha256:13fc02ab67d1465cbe75715d7de6a87c5319477eba2c0459c914bb5b698df766",
|
|
50
50
|
"platforms": {
|
|
51
|
-
"linux/
|
|
52
|
-
"linux/
|
|
51
|
+
"linux/amd64": "sha256:78883c59167af69c5b9433ef6cdfa35aab3833501bb85d830d95f24079a6c8d0",
|
|
52
|
+
"linux/arm64": "sha256:f27f09f8e661261c770976580d9861d4ce290eb1ac3030e6198f3c0a4987bd8a"
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
}
|
package/src/bin.js
CHANGED
|
@@ -15,13 +15,13 @@ import buildWasiCommand from './actions/buildWasiCommand.js';
|
|
|
15
15
|
import buildNode, { addonsInPackages, bundleNodeLoader, publishNodeData } from './actions/buildNode.js';
|
|
16
16
|
import buildNativeCommand from './actions/buildNativeCommand.js';
|
|
17
17
|
import buildApp, { appOutputPath, assertAppRunner, publishApp } from './actions/buildApp.js';
|
|
18
|
-
import { UPDATER_KEY, newUpdaterKey } from './actions/appUpdate.js';
|
|
19
18
|
import {
|
|
20
19
|
makeUploadKey, releaseSigningEnv, showUploadKeyPassword, useUploadKey,
|
|
21
20
|
} from './actions/uploadKey.js';
|
|
22
|
-
import {
|
|
23
|
-
import { publishIos } from './actions/publishIos.js';
|
|
21
|
+
import publish, { publishHelp, publishersOf } from './actions/publish.js';
|
|
24
22
|
import { iosDeviceOption } from './actions/iosDevice.js';
|
|
23
|
+
import addExtension from './actions/addExtension.js';
|
|
24
|
+
import addExtensionCommands from './actions/extensionCommands.js';
|
|
25
25
|
import buildExternal from './actions/buildExternal.js';
|
|
26
26
|
import buildPackageTypes from './actions/buildTypes.js';
|
|
27
27
|
import buildLib from './actions/buildLib.js';
|
|
@@ -167,14 +167,6 @@ const commandApp = program.command('app').description('build the project as an a
|
|
|
167
167
|
});
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
-
commandApp.command('updater-key')
|
|
171
|
-
.description("make the key pair that signs a desktop app's updates")
|
|
172
|
-
.action(() => {
|
|
173
|
-
const { publicKey, privateKey } = newUpdaterKey();
|
|
174
|
-
console.log(`crossbind: app.updater.publicKey in crossbind.config.js: ${publicKey}`);
|
|
175
|
-
console.log(`crossbind: ${UPDATER_KEY}, kept secret where the app is built: ${privateKey}`);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
170
|
commandApp.command('upload-key')
|
|
179
171
|
.description("make this machine's upload key, which signs an Android app's bundle for Google Play")
|
|
180
172
|
.option('--use <keystore>', 'make a key you have the upload key instead, once it opens with its password')
|
|
@@ -188,26 +180,26 @@ commandApp.command('upload-key')
|
|
|
188
180
|
});
|
|
189
181
|
|
|
190
182
|
commandApp.command('publish')
|
|
191
|
-
.
|
|
192
|
-
.
|
|
193
|
-
.
|
|
194
|
-
.
|
|
195
|
-
//
|
|
183
|
+
.usage('[store] [options]')
|
|
184
|
+
.description("build the app and send it to a store, as its publisher does, one of crossbind's own or of an extension's")
|
|
185
|
+
.addOption(new Option('-p, --platform <platform>', "the app's platform: without a store, the one that goes first for it (default: CROSSBIND_APP_PLATFORM, else for a store of desktop apps this machine's)").choices(APP_PLATFORMS))
|
|
186
|
+
.addHelpText('after', () => publishHelp(publishersOf(state.config?.extensions ?? [])))
|
|
187
|
+
// The store's own options, and Vite's, as --mode, which go on to Vite.
|
|
196
188
|
.allowUnknownOption()
|
|
197
189
|
.allowExcessArguments()
|
|
198
|
-
.action(async ({ platform
|
|
190
|
+
.action(async ({ platform }, command) => {
|
|
199
191
|
assertAppProject();
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
192
|
+
await publish(command.args, { platform, envPlatform: process.env.CROSSBIND_APP_PLATFORM, runVite });
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// crossbind app alone reads them, so that an extension's package that cannot be read stops no other command.
|
|
196
|
+
if (process.argv[2] === 'app') addExtensionCommands(commandApp);
|
|
197
|
+
|
|
198
|
+
program.command('add')
|
|
199
|
+
.description("install an app extension's package with the project's package manager and list it in crossbind.config.js's extensions")
|
|
200
|
+
.argument('<package>', "the package as a package manager takes it: its name, name@version or a path to it")
|
|
201
|
+
.action((spec) => {
|
|
202
|
+
addExtension(spec, { config: state.config });
|
|
211
203
|
});
|
|
212
204
|
|
|
213
205
|
program.command('licenses')
|
package/src/index.js
CHANGED
|
@@ -6,7 +6,8 @@ export { default as buildWasm } from './actions/buildWasm.js';
|
|
|
6
6
|
export {
|
|
7
7
|
default as buildApp, buildProjectApp, publishApp, appLayout, appOutputName, appTargetParams,
|
|
8
8
|
} from './actions/buildApp.js';
|
|
9
|
-
export { checkInstaller } from './actions/appInstaller.js';
|
|
9
|
+
export { checkInstaller, shipApp } from './actions/appInstaller.js';
|
|
10
|
+
export { resolveAppPageApi } from './utils/resolveAppHost.js';
|
|
10
11
|
export { default as openInSimulator, buildForSimulator } from './actions/iosSimulator.js';
|
|
11
12
|
export {
|
|
12
13
|
default as openOnIosDevice, buildForIosDevice, connectedIosDevice, iosDeviceAddress, iosDevServer,
|