crossbind 2.0.0-beta.65 → 2.0.0-beta.66
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/package.json +6 -1
- package/src/actions/androidDevice.js +1 -1
- package/src/actions/appInstaller.js +174 -0
- package/src/actions/appUpdate.js +68 -0
- package/src/actions/appleSigning.js +96 -0
- package/src/actions/buildApp.js +107 -32
- package/src/actions/createLib.js +10 -5
- package/src/actions/getCmakeParameters.js +4 -1
- package/src/actions/gradleProject.js +65 -4
- package/src/actions/licenses.js +31 -2
- package/src/actions/windowsSigning.js +96 -0
- package/src/actions/xcodeProject.js +44 -10
- package/src/app/index.d.ts +129 -0
- package/src/app/index.js +184 -0
- package/src/assets/toolchain-digests.json +20 -20
- package/src/bin.js +9 -0
- package/src/index.js +2 -1
- package/src/state/loadConfig.js +1 -1
- package/src/utils/appApiHeader.js +29 -0
- package/src/utils/appIcon.js +174 -0
- package/src/utils/appOptions.js +284 -0
- package/src/utils/writeIfChanged.js +1 -1
- package/vitest.config.js +14 -0
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// The app section of crossbind.config.js: what a user sees of an app, its name and, on the desktop, its window, and where
|
|
2
|
+
// its page opens files.
|
|
3
|
+
|
|
4
|
+
const WINDOW_DEFAULTS = { width: 1024, height: 768, minWidth: 0, minHeight: 0, maxWidth: 0, maxHeight: 0, resizable: true, fullscreen: false, backgroundColor: null };
|
|
5
|
+
// Larger than any display, and far within the int a host takes a size as.
|
|
6
|
+
const MAX_WINDOW_SIZE = 16384;
|
|
7
|
+
// Windows keeps each of a file version's four numbers in 16 bits.
|
|
8
|
+
const MAX_VERSION_NUMBER = 65535;
|
|
9
|
+
// Up to four numbers, then a semver prerelease or build part; none of its characters needs escaping in C or RC.
|
|
10
|
+
const WINDOWS_VERSION = /^(\d+(?:\.\d+){0,3})(?:[-+][0-9A-Za-z.+-]*)?$/;
|
|
11
|
+
// What app.openPath can let the page's shell.openPath open: the app's folders as app.paths() names them, and what a
|
|
12
|
+
// dialog picked during this run.
|
|
13
|
+
const OPEN_PATH_PLACES = ['data', 'cache', 'logs', 'picked'];
|
|
14
|
+
|
|
15
|
+
// An app's bundle id, which also names its folders on the desktop: app.id, else one made from the project's name.
|
|
16
|
+
export function appId(config) {
|
|
17
|
+
const id = config.app.id ?? `dev.crossbind.${config.general.name.replace(/[^A-Za-z0-9.-]/g, '-')}`;
|
|
18
|
+
// An empty part would let "." or ".." name the folder above the app's own.
|
|
19
|
+
if (typeof id !== 'string' || !/^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*$/.test(id)) {
|
|
20
|
+
throw new Error(`crossbind: app.id ${JSON.stringify(id)} is no bundle id, which takes dot-separated parts of letters, digits and hyphens, as com.example.app does.`);
|
|
21
|
+
}
|
|
22
|
+
return id;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The name the app shows: its window's title, its macOS menus, its Windows file description and the label under its
|
|
26
|
+
// icon. The project's name, without app.name.
|
|
27
|
+
export function appDisplayName(config) {
|
|
28
|
+
const isOwn = config.app.name !== undefined;
|
|
29
|
+
const name = isOwn ? config.app.name : config.general.name;
|
|
30
|
+
// The host takes it as a C string and a Windows resource string, which hold none of these as they are, and Android
|
|
31
|
+
// reads a label that starts with @ or ? as a resource.
|
|
32
|
+
if (typeof name !== 'string' || !name.trim() || /["\\\p{Cc}]/u.test(name) || /^[@?]/.test(name)) {
|
|
33
|
+
const rule = 'which is text without a quotation mark, a backslash or a control character, starting with neither @ nor ?';
|
|
34
|
+
throw new Error(isOwn
|
|
35
|
+
? `crossbind: app.name ${JSON.stringify(name)} is no name an app can show, ${rule}. Change it, or drop app.name to show general.name.`
|
|
36
|
+
: `crossbind: general.name ${JSON.stringify(name)}, which an app shows without app.name, is no name it can show, ${rule}. Set app.name in crossbind.config.js.`);
|
|
37
|
+
}
|
|
38
|
+
return name;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// The desktop window: the size of its page, the least it shrinks to and the most it grows to, whether it resizes and
|
|
42
|
+
// whether it opens full screen. Its backgroundColor, which a phone takes as well, is appBackgroundColor's.
|
|
43
|
+
export function appWindow(config) {
|
|
44
|
+
const window = config.app.window ?? {};
|
|
45
|
+
const keys = Object.keys(WINDOW_DEFAULTS).join(', ');
|
|
46
|
+
if (typeof window !== 'object' || Array.isArray(window)) {
|
|
47
|
+
throw new Error(`crossbind: app.window ${JSON.stringify(window)} is no object of ${keys}, as { width: 1024, height: 768 } is.`);
|
|
48
|
+
}
|
|
49
|
+
const unknown = Object.keys(window).filter((key) => !(key in WINDOW_DEFAULTS));
|
|
50
|
+
if (unknown.length > 0) throw new Error(`crossbind: app.window takes ${keys}; ${unknown.join(', ')} is none of them.`);
|
|
51
|
+
const options = { ...WINDOW_DEFAULTS, ...window };
|
|
52
|
+
['width', 'height', 'minWidth', 'minHeight', 'maxWidth', 'maxHeight'].forEach((key) => {
|
|
53
|
+
const least = /^(min|max)/.test(key) ? 0 : 1;
|
|
54
|
+
if (!Number.isInteger(options[key]) || options[key] < least || options[key] > MAX_WINDOW_SIZE) {
|
|
55
|
+
throw new Error(`crossbind: app.window.${key} ${JSON.stringify(options[key])} is no size, which is a whole number of pixels from ${least} to ${MAX_WINDOW_SIZE}.`);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
['resizable', 'fullscreen'].forEach((key) => {
|
|
59
|
+
if (typeof options[key] !== 'boolean') throw new Error(`crossbind: app.window.${key} ${JSON.stringify(options[key])} is no boolean: set it to true or false.`);
|
|
60
|
+
});
|
|
61
|
+
if (!options.resizable && (options.minWidth || options.minHeight || options.maxWidth || options.maxHeight)) {
|
|
62
|
+
throw new Error('crossbind: app.window.minWidth, minHeight, maxWidth and maxHeight limit a window that resizes: drop them, or set app.window.resizable to true.');
|
|
63
|
+
}
|
|
64
|
+
[['width', 'Width'], ['height', 'Height']].forEach(([size, side]) => {
|
|
65
|
+
const [least, most] = [options[`min${side}`], options[`max${side}`]];
|
|
66
|
+
if (least > options[size]) {
|
|
67
|
+
throw new Error(`crossbind: app.window.min${side} ${least} is above the ${size} the window opens with, ${options[size]}: lower it, or raise app.window.${size}.`);
|
|
68
|
+
}
|
|
69
|
+
if (most && most < options[size]) {
|
|
70
|
+
throw new Error(`crossbind: app.window.max${side} ${most} is below the ${size} the window opens with, ${options[size]}: raise it, or lower app.window.${size}.`);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
return options;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// The color an app shows until its page paints, and behind a page without a background of its own, on iOS and Android as
|
|
77
|
+
// well: app.window.backgroundColor, else null, which leaves the system's.
|
|
78
|
+
export function appBackgroundColor(config) {
|
|
79
|
+
const color = config.app.window?.backgroundColor ?? null;
|
|
80
|
+
if (color !== null && (typeof color !== 'string' || !/^#[0-9A-Fa-f]{6}$/.test(color))) {
|
|
81
|
+
throw new Error(`crossbind: app.window.backgroundColor ${JSON.stringify(color)} is no color of the form #rrggbb, as #1e1e1e is.`);
|
|
82
|
+
}
|
|
83
|
+
return color;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const PERMISSIONS = ['camera', 'microphone', 'location'];
|
|
87
|
+
|
|
88
|
+
// The devices the page may use, each with the reason for it, which macOS and iOS read from the Info.plist:
|
|
89
|
+
// app.permissions.
|
|
90
|
+
export function appPermissions(config) {
|
|
91
|
+
const permissions = config.app.permissions ?? {};
|
|
92
|
+
const names = PERMISSIONS.join(', ');
|
|
93
|
+
if (typeof permissions !== 'object' || Array.isArray(permissions)) {
|
|
94
|
+
throw new Error(`crossbind: app.permissions ${JSON.stringify(permissions)} is no object of ${names}, each with the reason the app uses it, as { camera: 'To scan barcodes' } is.`);
|
|
95
|
+
}
|
|
96
|
+
const unknown = Object.keys(permissions).filter((name) => !PERMISSIONS.includes(name));
|
|
97
|
+
if (unknown.length > 0) throw new Error(`crossbind: app.permissions takes ${names}; ${unknown.join(', ')} is none of them.`);
|
|
98
|
+
Object.entries(permissions).forEach(([name, reason]) => {
|
|
99
|
+
if (typeof reason !== 'string' || !reason.trim() || /\p{Cc}/u.test(reason)) {
|
|
100
|
+
throw new Error(`crossbind: app.permissions.${name} ${JSON.stringify(reason)} is no reason, which is text without control characters that the app's user reads when it asks for the ${name}, as 'To scan barcodes' is.`);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return permissions;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The reasons of app.permissions under the Info.plist keys macOS or iOS reads them from; macOS reads location's older
|
|
107
|
+
// key as well.
|
|
108
|
+
export function usageDescriptions(config, platform) {
|
|
109
|
+
const keys = {
|
|
110
|
+
camera: ['NSCameraUsageDescription'],
|
|
111
|
+
microphone: ['NSMicrophoneUsageDescription'],
|
|
112
|
+
location: ['NSLocationWhenInUseUsageDescription', ...(platform === 'darwin' ? ['NSLocationUsageDescription'] : [])],
|
|
113
|
+
};
|
|
114
|
+
return Object.fromEntries(Object.entries(appPermissions(config)).flatMap(([name, reason]) => keys[name].map((key) => [key, reason])));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// The version a Windows app's executable carries: package.json's, whose numbers are its file version.
|
|
118
|
+
function windowsVersion(config) {
|
|
119
|
+
const version = config.package?.version ?? '1.0.0';
|
|
120
|
+
const match = typeof version === 'string' ? WINDOWS_VERSION.exec(version) : null;
|
|
121
|
+
const numbers = match ? match[1].split('.').map(Number) : [];
|
|
122
|
+
if (!match || numbers.some((number) => number > MAX_VERSION_NUMBER)) {
|
|
123
|
+
throw new Error(`crossbind: package.json's version ${JSON.stringify(version)} is no version a Windows app can carry, which is up to four numbers from 0 to ${MAX_VERSION_NUMBER}, as 1.2.3 and 1.2.3-beta.1 are.`);
|
|
124
|
+
}
|
|
125
|
+
return { text: version, numbers: [...numbers, 0, 0, 0].slice(0, 4).join(',') };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Where the page's shell.openPath opens a file: nowhere unless app.openPath lists it, as opening a program runs it.
|
|
129
|
+
function appOpenPath(config) {
|
|
130
|
+
const places = config.app.openPath ?? [];
|
|
131
|
+
if (!Array.isArray(places)) {
|
|
132
|
+
throw new Error(`crossbind: app.openPath ${JSON.stringify(places)} is no list of ${OPEN_PATH_PLACES.join(', ')}, as ['logs', 'picked'] is.`);
|
|
133
|
+
}
|
|
134
|
+
const unknown = places.filter((place) => !OPEN_PATH_PLACES.includes(place));
|
|
135
|
+
if (unknown.length > 0) {
|
|
136
|
+
throw new Error(`crossbind: app.openPath takes ${OPEN_PATH_PLACES.join(', ')}; ${unknown.map((place) => JSON.stringify(place)).join(', ')} is none of them.`);
|
|
137
|
+
}
|
|
138
|
+
return places;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Schemes the web, the system or crossbind owns, which no app takes, and the keys of Windows' own classes, whose name a
|
|
142
|
+
// scheme's registry key would take.
|
|
143
|
+
const RESERVED_SCHEMES = [
|
|
144
|
+
'about', 'blob', 'content', 'crossbind', 'data', 'file', 'ftp', 'http', 'https', 'intent', 'javascript', 'mailto', 'sms', 'tel', 'ws', 'wss',
|
|
145
|
+
'appid', 'applications', 'clsid', 'directory', 'drive', 'folder', 'interface', 'mime', 'protocols', 'systemfileassociations', 'typelib', 'unknown',
|
|
146
|
+
'wow6432node',
|
|
147
|
+
];
|
|
148
|
+
|
|
149
|
+
// The schemes whose links open the app, as myapp://… does: lowercase, which is how Android matches them, and of two
|
|
150
|
+
// characters at least, as one would take a Windows path's drive for a scheme.
|
|
151
|
+
export function appUrlSchemes(config) {
|
|
152
|
+
const schemes = config.app.urlSchemes ?? [];
|
|
153
|
+
if (!Array.isArray(schemes)) {
|
|
154
|
+
throw new Error(`crossbind: app.urlSchemes ${JSON.stringify(schemes)} is no list of schemes, as ['myapp'] is.`);
|
|
155
|
+
}
|
|
156
|
+
for (const scheme of schemes) {
|
|
157
|
+
if (typeof scheme !== 'string' || !/^[a-z][a-z0-9+.-]+$/.test(scheme)) {
|
|
158
|
+
throw new Error(`crossbind: app.urlSchemes takes schemes of a lowercase letter, then one or more lowercase letters, digits, +, - and ., as myapp is; ${JSON.stringify(scheme)} is none.`);
|
|
159
|
+
}
|
|
160
|
+
if (RESERVED_SCHEMES.includes(scheme)) {
|
|
161
|
+
throw new Error(`crossbind: app.urlSchemes cannot take ${scheme}, which the web or the system owns: pick a scheme of the app's own.`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (new Set(schemes).size !== schemes.length) throw new Error(`crossbind: app.urlSchemes lists a scheme twice: ${JSON.stringify(schemes)}.`);
|
|
165
|
+
return schemes;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Semver's version: three numbers, then a prerelease and build metadata of dot-separated identifiers.
|
|
169
|
+
const SEMVER = /^\d+\.\d+\.\d+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$/;
|
|
170
|
+
// What an update's files are named after: characters an address carries as they are, and no leading dot.
|
|
171
|
+
const UPDATE_NAME = /^[A-Za-z0-9_~-][A-Za-z0-9._~-]*$/;
|
|
172
|
+
// The Ed25519 public keys of the points of small order, whatever the sign of x, whose signatures anyone can forge: the
|
|
173
|
+
// list libsodium's ge25519_has_small_order checks.
|
|
174
|
+
const SMALL_ORDER_KEYS = [
|
|
175
|
+
'0000000000000000000000000000000000000000000000000000000000000000',
|
|
176
|
+
'0100000000000000000000000000000000000000000000000000000000000000',
|
|
177
|
+
'26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05',
|
|
178
|
+
'c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a',
|
|
179
|
+
'ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
|
|
180
|
+
'edffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
|
|
181
|
+
'eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f',
|
|
182
|
+
].map((hex) => Buffer.from(hex, 'hex'));
|
|
183
|
+
|
|
184
|
+
const isSmallOrder = (key) => SMALL_ORDER_KEYS.some((point) => key.subarray(0, 31).equals(point.subarray(0, 31)) && (key[31] & 0x7f) === point[31]);
|
|
185
|
+
|
|
186
|
+
// Where a desktop app finds its updates, the key their signatures are checked with, and the version they are newer than:
|
|
187
|
+
// package.json's, which semver orders. Null without app.updater.
|
|
188
|
+
export function appUpdater(config) {
|
|
189
|
+
const updater = config.app.updater;
|
|
190
|
+
if (updater === undefined) return null;
|
|
191
|
+
const keys = 'url and publicKey';
|
|
192
|
+
if (updater === null || typeof updater !== 'object' || Array.isArray(updater)) {
|
|
193
|
+
throw new Error(`crossbind: app.updater ${JSON.stringify(updater)} is no object of ${keys}, as { url: 'https://example.com/updates/<name>-{platform}-{arch}.update.json', publicKey: '…' } is.`);
|
|
194
|
+
}
|
|
195
|
+
const unknown = Object.keys(updater).filter((key) => !['url', 'publicKey'].includes(key));
|
|
196
|
+
if (unknown.length > 0) throw new Error(`crossbind: app.updater takes ${keys}; ${unknown.join(', ')} is none of them.`);
|
|
197
|
+
const url = URL.canParse(updater.url) ? new URL(updater.url) : null;
|
|
198
|
+
const isLocal = url?.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
|
|
199
|
+
// The host takes the address as a C string, as it is written, and the update's file from beside the manifest.
|
|
200
|
+
if (!url || !(url.protocol === 'https:' || isLocal) || /[^\x21-\x7e]|["\\]/.test(updater.url) || url.pathname.endsWith('/')) {
|
|
201
|
+
throw new Error(`crossbind: app.updater.url ${JSON.stringify(updater.url)} is no https address of a manifest, as https://example.com/updates/<name>-{platform}-{arch}.update.json is; http serves localhost alone.`);
|
|
202
|
+
}
|
|
203
|
+
const publicKey = Buffer.from(typeof updater.publicKey === 'string' ? updater.publicKey : '', 'base64');
|
|
204
|
+
if (publicKey.length !== 32 || publicKey.toString('base64') !== updater.publicKey) {
|
|
205
|
+
throw new Error(`crossbind: app.updater.publicKey ${JSON.stringify(updater.publicKey)} is no Ed25519 public key, which \`crossbind app updater-key\` prints.`);
|
|
206
|
+
}
|
|
207
|
+
if (isSmallOrder(publicKey)) {
|
|
208
|
+
throw new Error(`crossbind: app.updater.publicKey ${JSON.stringify(updater.publicKey)} is a key anyone can forge signatures for, which no key pair has. Use the one \`crossbind app updater-key\` prints.`);
|
|
209
|
+
}
|
|
210
|
+
const version = config.package?.version ?? '1.0.0';
|
|
211
|
+
if (typeof version !== 'string' || !SEMVER.test(version)) {
|
|
212
|
+
throw new Error(`crossbind: package.json's version ${JSON.stringify(version)} is no semver version, as 1.2.3 and 1.2.3-beta.1 are, which an app that updates needs to tell a newer one.`);
|
|
213
|
+
}
|
|
214
|
+
if (!UPDATE_NAME.test(config.general.name)) {
|
|
215
|
+
throw new Error(`crossbind: general.name ${JSON.stringify(config.general.name)} names an app's update files, which an app fetches by name: letters, digits, and . _ ~ - alone, not starting with a dot. Change general.name in crossbind.config.js.`);
|
|
216
|
+
}
|
|
217
|
+
return { url: updater.url, publicKey, version };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// A desktop host's window: its page's size and limits, whether it resizes and whether it opens full screen. A maximum of
|
|
221
|
+
// one side alone leaves the other as large as any display.
|
|
222
|
+
function windowDefines(window) {
|
|
223
|
+
const most = window.maxWidth || window.maxHeight ? [
|
|
224
|
+
`#define CROSSBIND_APP_WINDOW_MAX_WIDTH ${window.maxWidth || MAX_WINDOW_SIZE}`,
|
|
225
|
+
`#define CROSSBIND_APP_WINDOW_MAX_HEIGHT ${window.maxHeight || MAX_WINDOW_SIZE}`,
|
|
226
|
+
] : [];
|
|
227
|
+
return [
|
|
228
|
+
`#define CROSSBIND_APP_WINDOW_WIDTH ${window.width}`,
|
|
229
|
+
`#define CROSSBIND_APP_WINDOW_HEIGHT ${window.height}`,
|
|
230
|
+
`#define CROSSBIND_APP_WINDOW_MIN_WIDTH ${window.minWidth}`,
|
|
231
|
+
`#define CROSSBIND_APP_WINDOW_MIN_HEIGHT ${window.minHeight}`,
|
|
232
|
+
`#define CROSSBIND_APP_WINDOW_RESIZABLE ${window.resizable ? 1 : 0}`,
|
|
233
|
+
...most,
|
|
234
|
+
...(window.fullscreen ? ['#define CROSSBIND_APP_WINDOW_FULLSCREEN 1'] : []),
|
|
235
|
+
];
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// The header an app's host compiles where its page opens files from, an iOS or desktop host the color it shows until
|
|
239
|
+
// its page paints and the devices the page may use, a desktop host its title, window, whether it keeps one instance and
|
|
240
|
+
// its updates, and a Windows host its version resource.
|
|
241
|
+
export function appHeader(config, platform) {
|
|
242
|
+
const comment = '// crossbind writes this file from crossbind.config.js and package.json.';
|
|
243
|
+
const openPath = `#define CROSSBIND_APP_OPEN_PATH {${appOpenPath(config).map((place) => `"${place}"`).join(', ')}}`;
|
|
244
|
+
const color = appBackgroundColor(config);
|
|
245
|
+
const permissions = appPermissions(config);
|
|
246
|
+
// An Android app's activity takes the color and the permissions from its Gradle project.
|
|
247
|
+
const page = platform === 'android' ? [] : [
|
|
248
|
+
...(color ? [`#define CROSSBIND_APP_BACKGROUND_COLOR 0x${color.slice(1).toLowerCase()}`] : []),
|
|
249
|
+
...PERMISSIONS.map((name) => `#define CROSSBIND_APP_PERMISSION_${name.toUpperCase()} ${name in permissions ? 1 : 0}`),
|
|
250
|
+
];
|
|
251
|
+
if (!['darwin', 'linux', 'win32'].includes(platform)) return [comment, openPath, ...page, ''].join('\n');
|
|
252
|
+
const lines = [
|
|
253
|
+
comment,
|
|
254
|
+
openPath,
|
|
255
|
+
...page,
|
|
256
|
+
`#define CROSSBIND_APP_ID "${appId(config)}"`,
|
|
257
|
+
`#define CROSSBIND_APP_NAME "${appDisplayName(config)}"`,
|
|
258
|
+
...windowDefines(appWindow(config)),
|
|
259
|
+
];
|
|
260
|
+
const urlSchemes = appUrlSchemes(config);
|
|
261
|
+
if (urlSchemes.length > 0) lines.push(`#define CROSSBIND_APP_URL_SCHEMES {${urlSchemes.map((scheme) => `"${scheme}"`).join(', ')}}`);
|
|
262
|
+
// A link the system opens a Windows or Linux app with starts it again, which reaches the app that runs this way; macOS
|
|
263
|
+
// hands the link to the app that runs.
|
|
264
|
+
const singleInstance = config.app.singleInstance ?? (urlSchemes.length > 0 && platform !== 'darwin');
|
|
265
|
+
if (typeof singleInstance !== 'boolean') {
|
|
266
|
+
throw new Error(`crossbind: app.singleInstance ${JSON.stringify(singleInstance)} is no boolean: set it to true to keep a desktop app to one instance, or to false to let each launch run.`);
|
|
267
|
+
}
|
|
268
|
+
if (singleInstance) lines.push('#define CROSSBIND_APP_SINGLE_INSTANCE 1');
|
|
269
|
+
const updater = appUpdater(config);
|
|
270
|
+
if (updater) {
|
|
271
|
+
lines.push(
|
|
272
|
+
`#define CROSSBIND_APP_UPDATER_URL "${updater.url}"`,
|
|
273
|
+
`#define CROSSBIND_APP_UPDATER_KEY {${[...updater.publicKey].map((byte) => `0x${byte.toString(16).padStart(2, '0')}`).join(', ')}}`,
|
|
274
|
+
`#define CROSSBIND_APP_UPDATER_VERSION "${updater.version}"`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
if (platform === 'win32') {
|
|
278
|
+
const version = windowsVersion(config);
|
|
279
|
+
lines.push(`#define CROSSBIND_APP_VERSION "${version.text}"`, `#define CROSSBIND_APP_VERSION_NUMBERS ${version.numbers}`);
|
|
280
|
+
// The build writes the icon beside this header.
|
|
281
|
+
if (config.app.icon !== undefined) lines.push('#define CROSSBIND_APP_ICON 1');
|
|
282
|
+
}
|
|
283
|
+
return [...lines, ''].join('\n');
|
|
284
|
+
}
|
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
|
|
4
4
|
export default function writeIfChanged(file, content) {
|
|
5
|
-
if (fs.existsSync(file) && fs.readFileSync(file
|
|
5
|
+
if (fs.existsSync(file) && fs.readFileSync(file).equals(Buffer.from(content))) return;
|
|
6
6
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
7
7
|
fs.writeFileSync(file, content);
|
|
8
8
|
}
|
package/vitest.config.js
CHANGED
|
@@ -27,6 +27,20 @@ export default defineConfig({
|
|
|
27
27
|
CROSSBIND_REMOTE_URL_LINUX: '',
|
|
28
28
|
CROSSBIND_REMOTE_URL_WINDOWS: '',
|
|
29
29
|
CROSSBIND_CLOUD_URL: 'http://127.0.0.1:9',
|
|
30
|
+
// Nor their signing keys: a macOS app here is signed ad hoc, and a Windows app not at all, unless a test names
|
|
31
|
+
// a key.
|
|
32
|
+
CROSSBIND_APPLE_SIGNING_IDENTITY: '',
|
|
33
|
+
CROSSBIND_APPLE_API_KEY: '',
|
|
34
|
+
CROSSBIND_APPLE_API_KEY_ID: '',
|
|
35
|
+
CROSSBIND_APPLE_API_ISSUER: '',
|
|
36
|
+
CROSSBIND_WINDOWS_SIGNING_STORETYPE: '',
|
|
37
|
+
CROSSBIND_WINDOWS_SIGNING_KEYSTORE: '',
|
|
38
|
+
CROSSBIND_WINDOWS_SIGNING_ALIAS: '',
|
|
39
|
+
CROSSBIND_WINDOWS_SIGNING_STOREPASS: '',
|
|
40
|
+
CROSSBIND_WINDOWS_SIGNING_KEYPASS: '',
|
|
41
|
+
CROSSBIND_WINDOWS_SIGNING_CERTFILE: '',
|
|
42
|
+
CROSSBIND_WINDOWS_SIGNING_TSAURL: '',
|
|
43
|
+
CROSSBIND_UPDATER_PRIVATE_KEY: '',
|
|
30
44
|
},
|
|
31
45
|
coverage: {
|
|
32
46
|
provider: 'v8',
|