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.
- package/README.md +1 -1
- package/package.json +70 -59
- package/src/actions/addExtension.js +196 -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 +177 -74
- package/src/actions/buildDependencies.js +3 -3
- package/src/actions/buildNode.js +6 -1
- package/src/actions/buildWasm.js +0 -9
- package/src/actions/createImportedBridges.js +10 -8
- package/src/actions/createInterface.js +40 -29
- package/src/actions/createLib.js +3 -7
- package/src/actions/extensionApi.js +298 -0
- package/src/actions/extensionCommands.js +41 -0
- package/src/actions/extensionsInApp.js +19 -0
- package/src/actions/getAllBridges.js +20 -5
- package/src/actions/getDependLibs.js +5 -0
- package/src/actions/getLinkInputs.js +2 -1
- package/src/actions/gradleProject.js +207 -16
- package/src/actions/iosDevice.js +2 -2
- package/src/actions/iosSimulator.js +3 -3
- package/src/actions/licenses.js +113 -58
- package/src/actions/linuxPackages.js +48 -0
- package/src/actions/mavenVerification.js +37 -0
- package/src/actions/{prepareConanDependencies.js → preparePackageSources.js} +13 -12
- package/src/actions/publish.js +220 -0
- package/src/actions/windowsSigning.js +1 -1
- package/src/actions/writeRuntimeEntry.js +2 -2
- package/src/actions/xcodeProject.js +60 -29
- package/src/actions/xcodeTargets.js +122 -0
- package/src/actions/xcodebuild.js +65 -0
- package/src/app/index.d.ts +144 -132
- package/src/app/index.js +147 -185
- package/src/assets/configure/install.sh +0 -0
- package/src/assets/node-import-hooks.mjs +7 -6
- package/src/assets/toolchain-digests.json +20 -20
- package/src/bin.js +37 -37
- package/src/index.js +4 -3
- package/src/integration/getCrossbindScript.js +3 -2
- package/src/integration/getDependFilePath.js +10 -10
- package/src/program.js +32 -3
- package/src/state/{attachConanDependencies.js → attachPackageSources.js} +20 -16
- package/src/state/index.js +8 -6
- package/src/state/loadConfig.js +26 -9
- package/src/state/refreshPackageSources.js +19 -0
- package/src/utils/appExtensions.js +777 -0
- package/src/utils/appFiles.js +58 -0
- package/src/utils/appLaunches.js +249 -0
- package/src/utils/appOptions.js +142 -112
- package/src/utils/appRustCrates.js +2 -2
- package/src/utils/appTargets.js +168 -0
- package/src/utils/bridgeTools.js +71 -0
- package/src/utils/configChanges.js +30 -0
- package/src/utils/configWriter.js +143 -26
- package/src/utils/cppDts.js +11 -11
- package/src/utils/extensionSteps.js +172 -0
- package/src/utils/nativeImports.js +9 -2
- package/src/utils/nodeDevBuild.js +2 -1
- package/src/utils/openUrl.js +2 -1
- package/src/utils/packageSourceApi.js +87 -0
- package/src/utils/packageSourceImport.js +69 -0
- package/src/utils/packageSourceInstall.js +162 -0
- package/src/utils/{conanStage.js → packageSourceStage.js} +64 -90
- package/src/utils/packageSources.js +108 -0
- package/src/utils/plist.js +5 -0
- package/src/utils/printableText.js +5 -0
- package/src/utils/resolveAppHost.js +7 -0
- package/src/utils/runPackageSource.js +251 -0
- package/src/utils/rustBridgeGen.js +3 -3
- package/src/utils/systemConfig.js +16 -3
- package/src/utils/targetToolchain.js +77 -0
- 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/state/refreshConanDependencies.js +0 -17
- package/src/utils/conanDependencies.js +0 -66
- package/src/utils/conanImport.js +0 -58
- package/src/utils/conanInstall.js +0 -154
- package/src/utils/conanProfile.js +0 -203
- package/src/utils/runConan.js +0 -257
package/src/utils/appOptions.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
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
2
|
// its page opens files.
|
|
3
|
+
import { carriedPath } from './appFiles.js';
|
|
3
4
|
|
|
4
|
-
const WINDOW_DEFAULTS = { width: 1024, height: 768, minWidth: 0, minHeight: 0, maxWidth: 0, maxHeight: 0, resizable: true, fullscreen: false, backgroundColor: null };
|
|
5
|
+
const WINDOW_DEFAULTS = { width: 1024, height: 768, minWidth: 0, minHeight: 0, maxWidth: 0, maxHeight: 0, resizable: true, fullscreen: false, backgroundColor: null, titleBar: 'visible' };
|
|
6
|
+
const TITLE_BARS = ['visible', 'hidden'];
|
|
5
7
|
// Larger than any display, and far within the int a host takes a size as.
|
|
6
8
|
const MAX_WINDOW_SIZE = 16384;
|
|
7
9
|
// Windows keeps each of a file version's four numbers in 16 bits.
|
|
8
10
|
const MAX_VERSION_NUMBER = 65535;
|
|
9
11
|
// Up to four numbers, then a semver prerelease or build part; none of its characters needs escaping in C or RC.
|
|
10
12
|
const WINDOWS_VERSION = /^(\d+(?:\.\d+){0,3})(?:[-+][0-9A-Za-z.+-]*)?$/;
|
|
11
|
-
//
|
|
12
|
-
|
|
13
|
-
const OPEN_PATH_PLACES = ['data', 'cache', 'logs', 'picked'];
|
|
13
|
+
// The platforms whose apps have a window of their own, and updates.
|
|
14
|
+
const DESKTOP = ['darwin', 'linux', 'win32'];
|
|
14
15
|
// Apple's app categories, which macOS shows an app under, each with the main categories of the freedesktop menu
|
|
15
16
|
// specification a Linux menu shows it under. An audio or a video app is in AudioVideo as well, as the specification asks.
|
|
16
17
|
const GAMES = ['', 'action-', 'adventure-', 'arcade-', 'board-', 'card-', 'casino-', 'dice-', 'educational-', 'family-', 'kids-', 'music-', 'puzzle-',
|
|
@@ -32,6 +33,18 @@ export function appId(config) {
|
|
|
32
33
|
return id;
|
|
33
34
|
}
|
|
34
35
|
|
|
36
|
+
// An Android application id: dot-separated parts of letters, digits and underscores that each start with a letter.
|
|
37
|
+
const ANDROID_APP_ID = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$/;
|
|
38
|
+
|
|
39
|
+
// An Android app's id: the project's app.id, else one made from the project's name.
|
|
40
|
+
export function androidAppId(config) {
|
|
41
|
+
const id = config.app.id ?? `dev.crossbind.${config.general.name.replace(/[^A-Za-z0-9.]/g, '_')}`;
|
|
42
|
+
if (!ANDROID_APP_ID.test(id)) {
|
|
43
|
+
throw new Error(`crossbind: ${JSON.stringify(id)} (app.id, else made from the project's name) is no Android application id, which takes dot-separated parts of letters, digits and underscores that each start with a letter, as com.example.app does. Set app.id in crossbind.config.js.`);
|
|
44
|
+
}
|
|
45
|
+
return id;
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
// The name the app shows: its window's title, its macOS menus, its Windows file description and the label under its
|
|
36
49
|
// icon. The project's name, without app.name.
|
|
37
50
|
export function appDisplayName(config) {
|
|
@@ -70,8 +83,9 @@ export function appCategory(config) {
|
|
|
70
83
|
return { darwin: `public.app-category.${category}`, linux: APP_CATEGORIES[category] };
|
|
71
84
|
}
|
|
72
85
|
|
|
73
|
-
// The desktop window: the size of its page, the least it shrinks to and the most it grows to, whether it resizes
|
|
74
|
-
// whether it opens full screen. Its backgroundColor, which a phone
|
|
86
|
+
// The desktop window: the size of its page, the least it shrinks to and the most it grows to, whether it resizes,
|
|
87
|
+
// whether it opens full screen and whether a macOS page lies under its title bar. Its backgroundColor, which a phone
|
|
88
|
+
// takes as well, is appBackgroundColor's.
|
|
75
89
|
export function appWindow(config) {
|
|
76
90
|
const window = config.app.window ?? {};
|
|
77
91
|
const keys = Object.keys(WINDOW_DEFAULTS).join(', ');
|
|
@@ -90,6 +104,9 @@ export function appWindow(config) {
|
|
|
90
104
|
['resizable', 'fullscreen'].forEach((key) => {
|
|
91
105
|
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.`);
|
|
92
106
|
});
|
|
107
|
+
if (!TITLE_BARS.includes(options.titleBar)) {
|
|
108
|
+
throw new Error(`crossbind: app.window.titleBar ${JSON.stringify(options.titleBar)} is neither 'visible' nor 'hidden': 'hidden' lays a macOS app's page under its title bar, whose buttons stay.`);
|
|
109
|
+
}
|
|
93
110
|
if (!options.resizable && (options.minWidth || options.minHeight || options.maxWidth || options.maxHeight)) {
|
|
94
111
|
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.');
|
|
95
112
|
}
|
|
@@ -105,16 +122,29 @@ export function appWindow(config) {
|
|
|
105
122
|
return options;
|
|
106
123
|
}
|
|
107
124
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
125
|
+
const COLOR = /^#[0-9A-Fa-f]{6}$/;
|
|
126
|
+
|
|
127
|
+
// The colors an app shows until its page paints, and behind a page without a background of its own, on iOS and Android as
|
|
128
|
+
// well: app.window.backgroundColor, one color, or { light, dark }, the one while the system is light and the one while
|
|
129
|
+
// it is dark, which a phone follows; else null, which leaves the system's.
|
|
130
|
+
function backgroundColors(config) {
|
|
111
131
|
const color = config.app.window?.backgroundColor ?? null;
|
|
112
|
-
if (color
|
|
113
|
-
|
|
132
|
+
if (color === null) return null;
|
|
133
|
+
if (typeof color === 'string' && COLOR.test(color)) return { light: color, dark: null };
|
|
134
|
+
const isPair = typeof color === 'object' && !Array.isArray(color) && Object.keys(color).sort().join() === 'dark,light'
|
|
135
|
+
&& [color.light, color.dark].every((one) => typeof one === 'string' && COLOR.test(one));
|
|
136
|
+
if (!isPair) {
|
|
137
|
+
throw new Error(`crossbind: app.window.backgroundColor ${JSON.stringify(color)} is no color of the form #rrggbb, as #1e1e1e is, nor { light, dark } of two, as { light: '#f2f2f2', dark: '#1e1e1e' } is.`);
|
|
114
138
|
}
|
|
115
139
|
return color;
|
|
116
140
|
}
|
|
117
141
|
|
|
142
|
+
// The color, or the light one of the two, which a desktop shows whatever its system's appearance.
|
|
143
|
+
export const appBackgroundColor = (config) => backgroundColors(config)?.light ?? null;
|
|
144
|
+
|
|
145
|
+
// The color an iOS or Android app shows while its system is dark, where app.window.backgroundColor gives one.
|
|
146
|
+
export const appDarkBackgroundColor = (config) => backgroundColors(config)?.dark ?? null;
|
|
147
|
+
|
|
118
148
|
const PERMISSIONS = ['camera', 'microphone', 'location'];
|
|
119
149
|
|
|
120
150
|
// The devices the page may use, each with the reason for it, which macOS and iOS read from the Info.plist:
|
|
@@ -158,18 +188,20 @@ export const PRIVACY_CATEGORIES = {
|
|
|
158
188
|
|
|
159
189
|
// The reasons an iOS app's privacy manifest gives for the required reason API it calls: app.privacy, each category with
|
|
160
190
|
// Apple's reasons that fit, as { fileTimestamp: ['C617.1'] }.
|
|
161
|
-
export
|
|
162
|
-
|
|
191
|
+
export const appPrivacy = (config) => checkedPrivacy(config.app.privacy ?? {}, 'app.privacy');
|
|
192
|
+
|
|
193
|
+
// Reasons for Apple's required reason API, as app.privacy gives them and an extension's ios.privacy does.
|
|
194
|
+
export function checkedPrivacy(privacy, where) {
|
|
163
195
|
const names = Object.keys(PRIVACY_CATEGORIES).join(', ');
|
|
164
196
|
if (typeof privacy !== 'object' || privacy === null || Array.isArray(privacy)) {
|
|
165
|
-
throw new Error(`crossbind:
|
|
197
|
+
throw new Error(`crossbind: ${where} ${JSON.stringify(privacy)} is no object of ${names}, each with Apple's reasons for it, as { fileTimestamp: ['C617.1'] } is.`);
|
|
166
198
|
}
|
|
167
199
|
const unknown = Object.keys(privacy).filter((name) => !Object.hasOwn(PRIVACY_CATEGORIES, name));
|
|
168
|
-
if (unknown.length > 0) throw new Error(`crossbind:
|
|
200
|
+
if (unknown.length > 0) throw new Error(`crossbind: ${where} takes ${names}; ${unknown.join(', ')} is none of them.`);
|
|
169
201
|
Object.entries(privacy).forEach(([name, reasons]) => {
|
|
170
202
|
const allowed = PRIVACY_CATEGORIES[name].reasons;
|
|
171
203
|
if (!Array.isArray(reasons) || reasons.length === 0 || reasons.some((reason) => !allowed.includes(reason))) {
|
|
172
|
-
throw new Error(`crossbind:
|
|
204
|
+
throw new Error(`crossbind: ${where}.${name} ${JSON.stringify(reasons)} is no list of Apple's reasons for it, which are ${allowed.join(', ')}; Apple's "Describing use of required reason API" says what each covers.`);
|
|
173
205
|
}
|
|
174
206
|
});
|
|
175
207
|
return privacy;
|
|
@@ -186,6 +218,40 @@ export function appEncryption(config) {
|
|
|
186
218
|
return encryption;
|
|
187
219
|
}
|
|
188
220
|
|
|
221
|
+
const IOS_OPTIONS = ['multipleWindows', 'appGroup'];
|
|
222
|
+
// An App Group of iOS's: group. and a bundle id's dot-separated parts.
|
|
223
|
+
const APP_GROUP = /^group(\.[A-Za-z0-9-]+)+$/;
|
|
224
|
+
|
|
225
|
+
function iosOptions(config) {
|
|
226
|
+
const ios = config.app.ios ?? {};
|
|
227
|
+
if (typeof ios !== 'object' || Array.isArray(ios)) {
|
|
228
|
+
throw new Error(`crossbind: app.ios ${JSON.stringify(ios)} is no object of multipleWindows and appGroup, as { multipleWindows: true } is.`);
|
|
229
|
+
}
|
|
230
|
+
const unknown = Object.keys(ios).filter((key) => !IOS_OPTIONS.includes(key));
|
|
231
|
+
if (unknown.length > 0) throw new Error(`crossbind: app.ios takes multipleWindows and appGroup; ${unknown.join(', ')} is none of them.`);
|
|
232
|
+
return ios;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Whether an iPad app's user, and its page, open more windows of it, as app.ios.multipleWindows says: iOS shows an
|
|
236
|
+
// iPhone's one window alone.
|
|
237
|
+
export function iosMultipleWindows(config) {
|
|
238
|
+
const { multipleWindows = false } = iosOptions(config);
|
|
239
|
+
if (typeof multipleWindows !== 'boolean') {
|
|
240
|
+
throw new Error(`crossbind: app.ios.multipleWindows ${JSON.stringify(multipleWindows)} is no boolean: set it to true for an iPad app's windows, or leave it out.`);
|
|
241
|
+
}
|
|
242
|
+
return multipleWindows;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// The App Group an iOS app shares with the targets its extensions add, as app.ios.appGroup names it, else group. and the
|
|
246
|
+
// app's id.
|
|
247
|
+
export function iosAppGroup(config) {
|
|
248
|
+
const { appGroup = `group.${appId(config)}` } = iosOptions(config);
|
|
249
|
+
if (typeof appGroup !== 'string' || !APP_GROUP.test(appGroup)) {
|
|
250
|
+
throw new Error(`crossbind: app.ios.appGroup ${JSON.stringify(appGroup)} is no App Group, which is group. and a bundle id's parts, as group.com.example.notes is.`);
|
|
251
|
+
}
|
|
252
|
+
return appGroup;
|
|
253
|
+
}
|
|
254
|
+
|
|
189
255
|
// The version a Windows app's executable carries: package.json's, whose numbers are its file version.
|
|
190
256
|
function windowsVersion(config) {
|
|
191
257
|
const version = config.package?.version ?? '1.0.0';
|
|
@@ -197,17 +263,13 @@ function windowsVersion(config) {
|
|
|
197
263
|
return { text: version, numbers: [...numbers, 0, 0, 0].slice(0, 4).join(',') };
|
|
198
264
|
}
|
|
199
265
|
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (unknown.length > 0) {
|
|
208
|
-
throw new Error(`crossbind: app.openPath takes ${OPEN_PATH_PLACES.join(', ')}; ${unknown.map((place) => JSON.stringify(place)).join(', ')} is none of them.`);
|
|
209
|
-
}
|
|
210
|
-
return places;
|
|
266
|
+
// The files and folders of the project the app carries for its own native code, as app.files lists them: inside the
|
|
267
|
+
// project's folder, which keep their places in the app's files/.
|
|
268
|
+
export function appFiles(config) {
|
|
269
|
+
const files = config.app.files ?? [];
|
|
270
|
+
if (!Array.isArray(files)) throw new Error(`crossbind: app.files ${JSON.stringify(files)} is no list of the project's files and folders, as ['seed.sqlite'] is.`);
|
|
271
|
+
const root = config.paths.project;
|
|
272
|
+
return files.map((file) => carriedPath('app.files', root, file, "project's"));
|
|
211
273
|
}
|
|
212
274
|
|
|
213
275
|
// Schemes the web, the system or crossbind owns, which no app takes, and the keys of Windows' own classes, whose name a
|
|
@@ -218,79 +280,59 @@ const RESERVED_SCHEMES = [
|
|
|
218
280
|
'wow6432node',
|
|
219
281
|
];
|
|
220
282
|
|
|
221
|
-
// The schemes whose links open the app, as myapp://… does: lowercase, which is how Android matches them, and
|
|
222
|
-
// characters at least, as one would take a Windows path's drive for a scheme.
|
|
223
|
-
export function
|
|
224
|
-
const schemes = config.app.urlSchemes ?? [];
|
|
283
|
+
// The schemes of a list whose links open the app, as myapp://… does: lowercase, which is how Android matches them, and
|
|
284
|
+
// of two characters at least, as one would take a Windows path's drive for a scheme.
|
|
285
|
+
export function checkedUrlSchemes(schemes, where) {
|
|
225
286
|
if (!Array.isArray(schemes)) {
|
|
226
|
-
throw new Error(`crossbind:
|
|
287
|
+
throw new Error(`crossbind: ${where} ${JSON.stringify(schemes)} is no list of schemes, as ['myapp'] is.`);
|
|
227
288
|
}
|
|
228
289
|
for (const scheme of schemes) {
|
|
229
290
|
if (typeof scheme !== 'string' || !/^[a-z][a-z0-9+.-]+$/.test(scheme)) {
|
|
230
|
-
throw new Error(`crossbind:
|
|
291
|
+
throw new Error(`crossbind: ${where} takes schemes of a lowercase letter, then one or more lowercase letters, digits, +, - and ., as myapp is; ${JSON.stringify(scheme)} is none.`);
|
|
231
292
|
}
|
|
232
293
|
if (RESERVED_SCHEMES.includes(scheme)) {
|
|
233
|
-
throw new Error(`crossbind:
|
|
294
|
+
throw new Error(`crossbind: ${where} cannot take ${scheme}, which the web or the system owns: pick a scheme of the app's own.`);
|
|
234
295
|
}
|
|
235
296
|
}
|
|
236
|
-
if (new Set(schemes).size !== schemes.length) throw new Error(`crossbind:
|
|
297
|
+
if (new Set(schemes).size !== schemes.length) throw new Error(`crossbind: ${where} lists a scheme twice: ${JSON.stringify(schemes)}.`);
|
|
237
298
|
return schemes;
|
|
238
299
|
}
|
|
239
300
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
//
|
|
243
|
-
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
//
|
|
260
|
-
export
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
const url = URL.canParse(updater.url) ? new URL(updater.url) : null;
|
|
270
|
-
const isLocal = url?.protocol === 'http:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname);
|
|
271
|
-
// The host takes the address as a C string, as it is written, and the update's file from beside the manifest.
|
|
272
|
-
if (!url || !(url.protocol === 'https:' || isLocal) || /[^\x21-\x7e]|["\\]/.test(updater.url) || url.pathname.endsWith('/')) {
|
|
273
|
-
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.`);
|
|
274
|
-
}
|
|
275
|
-
const publicKey = Buffer.from(typeof updater.publicKey === 'string' ? updater.publicKey : '', 'base64');
|
|
276
|
-
if (publicKey.length !== 32 || publicKey.toString('base64') !== updater.publicKey) {
|
|
277
|
-
throw new Error(`crossbind: app.updater.publicKey ${JSON.stringify(updater.publicKey)} is no Ed25519 public key, which \`crossbind app updater-key\` prints.`);
|
|
278
|
-
}
|
|
279
|
-
if (isSmallOrder(publicKey)) {
|
|
280
|
-
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.`);
|
|
281
|
-
}
|
|
282
|
-
const version = config.package?.version ?? '1.0.0';
|
|
283
|
-
if (typeof version !== 'string' || !SEMVER.test(version)) {
|
|
284
|
-
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.`);
|
|
285
|
-
}
|
|
286
|
-
if (!UPDATE_NAME.test(config.general.name)) {
|
|
287
|
-
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.`);
|
|
301
|
+
const ownerName = (extension) => (extension === null ? 'app.urlSchemes' : `the extension ${extension}`);
|
|
302
|
+
|
|
303
|
+
// The schemes whose links open the app, each with the extension that declared it, whose native code gets its links, or
|
|
304
|
+
// null for those of app.urlSchemes, whose links the host's links hands the page. Two owners of one scheme stop the build,
|
|
305
|
+
// naming both.
|
|
306
|
+
export function urlSchemeOwners(config) {
|
|
307
|
+
const owned = [
|
|
308
|
+
...checkedUrlSchemes(config.app.urlSchemes ?? [], 'app.urlSchemes').map((scheme) => ({ scheme, extension: null })),
|
|
309
|
+
...(config.extensions ?? []).flatMap(({ name, app }) => (app.urlSchemes ?? []).map((scheme) => ({ scheme, extension: name }))),
|
|
310
|
+
];
|
|
311
|
+
owned.forEach(({ scheme, extension }, index) => {
|
|
312
|
+
const first = owned.findIndex((other) => other.scheme === scheme);
|
|
313
|
+
if (first !== index) {
|
|
314
|
+
throw new Error(`crossbind: ${ownerName(owned[first].extension)} and ${ownerName(extension)} both claim the scheme ${scheme}, whose links one alone gets.`);
|
|
315
|
+
}
|
|
316
|
+
});
|
|
317
|
+
return owned;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// The schemes whose links open the app: app.urlSchemes's, then its extensions'.
|
|
321
|
+
export const appUrlSchemes = (config) => urlSchemeOwners(config).map(({ scheme }) => scheme);
|
|
322
|
+
|
|
323
|
+
// The installers app.installers names, which alone of each desktop platform's make what installs its app; null without
|
|
324
|
+
// it, when every one does.
|
|
325
|
+
export function appInstallers(config) {
|
|
326
|
+
const { installers } = config.app;
|
|
327
|
+
if (installers === undefined) return null;
|
|
328
|
+
if (!Array.isArray(installers) || !installers.every((name) => typeof name === 'string') || new Set(installers).size !== installers.length) {
|
|
329
|
+
throw new Error(`crossbind: app.installers ${JSON.stringify(installers)} is no list of installers' names, each once.`);
|
|
288
330
|
}
|
|
289
|
-
return
|
|
331
|
+
return installers;
|
|
290
332
|
}
|
|
291
333
|
|
|
292
|
-
// A desktop host's window: its page's size and limits, whether it resizes
|
|
293
|
-
// one side alone leaves the other as large as any display.
|
|
334
|
+
// A desktop host's window: its page's size and limits, whether it resizes, whether it opens full screen and whether its
|
|
335
|
+
// page lies under its title bar. A maximum of one side alone leaves the other as large as any display.
|
|
294
336
|
function windowDefines(window) {
|
|
295
337
|
const most = window.maxWidth || window.maxHeight ? [
|
|
296
338
|
`#define CROSSBIND_APP_WINDOW_MAX_WIDTH ${window.maxWidth || MAX_WINDOW_SIZE}`,
|
|
@@ -304,52 +346,40 @@ function windowDefines(window) {
|
|
|
304
346
|
`#define CROSSBIND_APP_WINDOW_RESIZABLE ${window.resizable ? 1 : 0}`,
|
|
305
347
|
...most,
|
|
306
348
|
...(window.fullscreen ? ['#define CROSSBIND_APP_WINDOW_FULLSCREEN 1'] : []),
|
|
349
|
+
...(window.titleBar === 'hidden' ? ['#define CROSSBIND_APP_WINDOW_TITLE_BAR_HIDDEN 1'] : []),
|
|
307
350
|
];
|
|
308
351
|
}
|
|
309
352
|
|
|
310
|
-
// The header an app's host compiles
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
export function appHeader(config, platform) {
|
|
353
|
+
// The header an app's host compiles its id and name with, an iOS or desktop host the color it shows until its page paints
|
|
354
|
+
// and the devices the page may use, an iOS host the App Group of its extensions' targets, a desktop host its window, and a
|
|
355
|
+
// Windows host its version resource.
|
|
356
|
+
export function appHeader(config, platform, appGroup = null) {
|
|
314
357
|
const comment = '// crossbind writes this file from crossbind.config.js and package.json.';
|
|
315
|
-
const
|
|
358
|
+
const id = `#define CROSSBIND_APP_ID "${platform === 'android' ? androidAppId(config) : appId(config)}"`;
|
|
359
|
+
const name = `#define CROSSBIND_APP_NAME "${appDisplayName(config)}"`;
|
|
316
360
|
const color = appBackgroundColor(config);
|
|
361
|
+
const dark = platform === 'ios' ? appDarkBackgroundColor(config) : null;
|
|
317
362
|
const permissions = appPermissions(config);
|
|
318
363
|
// An Android app's activity takes the color and the permissions from its Gradle project.
|
|
319
364
|
const page = platform === 'android' ? [] : [
|
|
320
365
|
...(color ? [`#define CROSSBIND_APP_BACKGROUND_COLOR 0x${color.slice(1).toLowerCase()}`] : []),
|
|
366
|
+
...(dark ? [`#define CROSSBIND_APP_BACKGROUND_COLOR_DARK 0x${dark.slice(1).toLowerCase()}`] : []),
|
|
321
367
|
...PERMISSIONS.map((name) => `#define CROSSBIND_APP_PERMISSION_${name.toUpperCase()} ${name in permissions ? 1 : 0}`),
|
|
322
368
|
];
|
|
323
|
-
|
|
369
|
+
const windows = platform === 'ios' && iosMultipleWindows(config) ? ['#define CROSSBIND_APP_IOS_MULTIPLE_WINDOWS 1'] : [];
|
|
370
|
+
const group = platform === 'ios' && appGroup ? [`#define CROSSBIND_APP_GROUP "${appGroup}"`] : [];
|
|
371
|
+
if (!DESKTOP.includes(platform)) return [comment, ...page, ...windows, ...group, id, name, ''].join('\n');
|
|
324
372
|
// The category goes to the Info.plist and the desktop entry, which the build writes later; any desktop refuses a
|
|
325
373
|
// wrong one here.
|
|
326
374
|
const copyright = appCopyright(config);
|
|
327
375
|
appCategory(config);
|
|
328
376
|
const lines = [
|
|
329
377
|
comment,
|
|
330
|
-
openPath,
|
|
331
378
|
...page,
|
|
332
|
-
|
|
333
|
-
|
|
379
|
+
id,
|
|
380
|
+
name,
|
|
334
381
|
...windowDefines(appWindow(config)),
|
|
335
382
|
];
|
|
336
|
-
const urlSchemes = appUrlSchemes(config);
|
|
337
|
-
if (urlSchemes.length > 0) lines.push(`#define CROSSBIND_APP_URL_SCHEMES {${urlSchemes.map((scheme) => `"${scheme}"`).join(', ')}}`);
|
|
338
|
-
// A link the system opens a Windows or Linux app with starts it again, which reaches the app that runs this way; macOS
|
|
339
|
-
// hands the link to the app that runs.
|
|
340
|
-
const singleInstance = config.app.singleInstance ?? (urlSchemes.length > 0 && platform !== 'darwin');
|
|
341
|
-
if (typeof singleInstance !== 'boolean') {
|
|
342
|
-
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.`);
|
|
343
|
-
}
|
|
344
|
-
if (singleInstance) lines.push('#define CROSSBIND_APP_SINGLE_INSTANCE 1');
|
|
345
|
-
const updater = appUpdater(config);
|
|
346
|
-
if (updater) {
|
|
347
|
-
lines.push(
|
|
348
|
-
`#define CROSSBIND_APP_UPDATER_URL "${updater.url}"`,
|
|
349
|
-
`#define CROSSBIND_APP_UPDATER_KEY {${[...updater.publicKey].map((byte) => `0x${byte.toString(16).padStart(2, '0')}`).join(', ')}}`,
|
|
350
|
-
`#define CROSSBIND_APP_UPDATER_VERSION "${updater.version}"`,
|
|
351
|
-
);
|
|
352
|
-
}
|
|
353
383
|
if (platform === 'win32') {
|
|
354
384
|
const version = windowsVersion(config);
|
|
355
385
|
lines.push(`#define CROSSBIND_APP_VERSION "${version.text}"`, `#define CROSSBIND_APP_VERSION_NUMBERS ${version.numbers}`);
|
|
@@ -23,9 +23,9 @@ export default function buildAppRustCrates(target, cacheDir, cargoDependencies =
|
|
|
23
23
|
.map((e) => e.name);
|
|
24
24
|
|
|
25
25
|
// Prune bridges whose source of truth is gone. An app-local surface whose .rs was deleted
|
|
26
|
-
// breaks cargo on the missing #[path] target; a crate_ bridge whose
|
|
26
|
+
// breaks cargo on the missing #[path] target; a crate_ bridge whose packages.cargo entry
|
|
27
27
|
// was removed silently keeps dead registrations in every later binary. crate_ pruning only
|
|
28
|
-
// runs when the caller passes the
|
|
28
|
+
// runs when the caller passes the crates declared now - null means "don't judge".
|
|
29
29
|
crates = crates.filter((name) => {
|
|
30
30
|
const dir = `${root}/${name}`;
|
|
31
31
|
const libRs = `${dir}/src/lib.rs`;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { IOS_DEPLOYMENT_TARGET } from './iosToolchain.js';
|
|
2
|
+
|
|
3
|
+
// Apple's app extensions an iOS app's extensions add, each a target of the app's Xcode project of its own, by their
|
|
4
|
+
// kinds: the extension point each runs at, which an other target names itself, the options it needs or may give, and
|
|
5
|
+
// the frameworks iOS starts it with, which its code may name nothing of, so that the linker would leave them out.
|
|
6
|
+
const KINDS = {
|
|
7
|
+
widget: { point: 'com.apple.widgetkit-extension', needs: [], takes: [] },
|
|
8
|
+
share: { point: 'com.apple.share-services', needs: ['principalClass', 'activation'], takes: [] },
|
|
9
|
+
'notification-service': { point: 'com.apple.usernotifications.service', needs: ['principalClass'], takes: [] },
|
|
10
|
+
'notification-content': {
|
|
11
|
+
point: 'com.apple.usernotifications.content-extension', needs: ['principalClass', 'categories'], takes: [], frameworks: ['UserNotifications', 'UserNotificationsUI'],
|
|
12
|
+
},
|
|
13
|
+
other: { point: null, needs: ['extensionPoint'], takes: ['principalClass'] },
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// The frameworks a target links: its kind's, and its own.
|
|
17
|
+
export const targetFrameworks = (target) => [...new Set([...(KINDS[target.kind].frameworks ?? []), ...(target.frameworks ?? [])])];
|
|
18
|
+
|
|
19
|
+
// The widget targets whose Swift names no containerBackground, through which iOS 17 asks a widget's view for its
|
|
20
|
+
// background: a debug build draws "Please adopt containerBackground API" in place of one without it. read(target, file)
|
|
21
|
+
// gives a source's text.
|
|
22
|
+
export const widgetsWithoutBackground = (targets, read) => targets
|
|
23
|
+
.filter((target) => target.kind === 'widget')
|
|
24
|
+
.filter((target) => !target.sources.some((file) => file.endsWith('.swift') && read(target, file).includes('containerBackground')))
|
|
25
|
+
.map((target) => target.name);
|
|
26
|
+
const KIND_KEYS = ['principalClass', 'activation', 'categories', 'extensionPoint'];
|
|
27
|
+
const TARGET_KEYS = ['name', 'kind', 'sources', 'resources', 'displayName', 'plist', 'entitlements', 'frameworks', 'privacy', 'appGroup',
|
|
28
|
+
'deploymentTarget', ...KIND_KEYS, 'attributes'];
|
|
29
|
+
const NEEDED = {
|
|
30
|
+
principalClass: 'the class iOS makes as it starts the target',
|
|
31
|
+
activation: "what of the share sheet's items it takes, as { images: 10 }",
|
|
32
|
+
categories: 'the notification categories it shows',
|
|
33
|
+
extensionPoint: 'the extension point iOS runs it at, as com.apple.keyboard-service',
|
|
34
|
+
};
|
|
35
|
+
// What of the share sheet's items a share extension takes, by the keys of Apple's activation rule: a count of each kind,
|
|
36
|
+
// and text or none.
|
|
37
|
+
const ACTIVATION = {
|
|
38
|
+
attachments: 'NSExtensionActivationSupportsAttachmentsWithMaxCount',
|
|
39
|
+
files: 'NSExtensionActivationSupportsFileWithMaxCount',
|
|
40
|
+
images: 'NSExtensionActivationSupportsImageWithMaxCount',
|
|
41
|
+
movies: 'NSExtensionActivationSupportsMovieWithMaxCount',
|
|
42
|
+
text: 'NSExtensionActivationSupportsText',
|
|
43
|
+
urls: 'NSExtensionActivationSupportsWebURLWithMaxCount',
|
|
44
|
+
webPages: 'NSExtensionActivationSupportsWebPageWithMaxCount',
|
|
45
|
+
};
|
|
46
|
+
// The attributes crossbind writes from a target's options, and the Info.plist keys it writes for every target, the App
|
|
47
|
+
// Group, which the target's code reads, of one that asks for it.
|
|
48
|
+
const WRITTEN_ATTRIBUTES = { NSExtensionActivationRule: 'activation', UNNotificationExtensionCategory: 'categories' };
|
|
49
|
+
const APP_GROUP_KEY = 'CrossbindAppGroup';
|
|
50
|
+
const WRITTEN_KEYS = ['CFBundleDevelopmentRegion', 'CFBundleDisplayName', 'CFBundleExecutable', 'CFBundleIdentifier', 'CFBundleInfoDictionaryVersion',
|
|
51
|
+
'CFBundleName', 'CFBundlePackageType', 'CFBundleShortVersionString', 'CFBundleVersion', 'NSExtension', APP_GROUP_KEY];
|
|
52
|
+
|
|
53
|
+
const NAME = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
54
|
+
const CLASS = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
55
|
+
const EXTENSION_POINT = /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+$/;
|
|
56
|
+
const VERSION = /^\d+\.\d+$/;
|
|
57
|
+
// ExtensionKit's points, whose extensions are of another product, in the app's Extensions, which crossbind makes none of.
|
|
58
|
+
const EXTENSION_KIT_POINTS = ['com.apple.appintents-extension'];
|
|
59
|
+
// What a target compiles apart from its headers, and the sources of Objective-C, beside which a class goes by its own name.
|
|
60
|
+
export const isHeader = (file) => /\.(h|hpp)$/.test(file);
|
|
61
|
+
const isObjectiveC = (file) => /\.(m|mm)$/.test(file);
|
|
62
|
+
const kindOf = (kind) => `${/^[aeiou]/.test(kind) ? 'an' : 'a'} ${kind} target`;
|
|
63
|
+
|
|
64
|
+
const isBelow = (version, floor) => {
|
|
65
|
+
const [major, minor] = version.split('.').map(Number);
|
|
66
|
+
const [floorMajor, floorMinor] = floor.split('.').map(Number);
|
|
67
|
+
return major < floorMajor || (major === floorMajor && minor < floorMinor);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// A target's name and kind, of the keys a target takes alone.
|
|
71
|
+
export function checkedTargetKind(at, target) {
|
|
72
|
+
const unknown = Object.keys(target).find((key) => !TARGET_KEYS.includes(key));
|
|
73
|
+
if (unknown) throw new Error(`crossbind: ${at} takes ${TARGET_KEYS.join(', ')}; ${unknown} is none of them.`);
|
|
74
|
+
if (typeof target.name !== 'string' || !NAME.test(target.name)) {
|
|
75
|
+
throw new Error(`crossbind: ${at}.name ${JSON.stringify(target.name)} is no name of lowercase letters and digits, which dashes may join.`);
|
|
76
|
+
}
|
|
77
|
+
if (!Object.hasOwn(KINDS, target.kind)) throw new Error(`crossbind: ${at}.kind ${JSON.stringify(target.kind)} is none of ${Object.keys(KINDS).join(', ')}.`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function checkedActivation(at, activation) {
|
|
81
|
+
const isTaken = (key, value) => Object.hasOwn(ACTIVATION, key) && (key === 'text' ? value === true : Number.isInteger(value) && value >= 1);
|
|
82
|
+
if (activation === null || typeof activation !== 'object' || Array.isArray(activation) || Object.keys(activation).length === 0
|
|
83
|
+
|| !Object.entries(activation).every(([key, value]) => isTaken(key, value))) {
|
|
84
|
+
throw new Error(`crossbind: ${at}.activation is no object of ${Object.keys(ACTIVATION).join(', ')}, each a count from 1 and text true, as { images: 10, urls: 1 } is.`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// What a target's kind takes and the rest of its own keys, once its sources and the values every part takes are checked.
|
|
89
|
+
export function checkedTargetOptions(at, target, sources) {
|
|
90
|
+
const { kind } = target;
|
|
91
|
+
for (const key of KIND_KEYS) {
|
|
92
|
+
const isGiven = target[key] !== undefined;
|
|
93
|
+
if (isGiven && !KINDS[kind].needs.includes(key) && !KINDS[kind].takes.includes(key)) throw new Error(`crossbind: ${at}, ${kindOf(kind)}, takes no ${key}.`);
|
|
94
|
+
if (!isGiven && KINDS[kind].needs.includes(key)) throw new Error(`crossbind: ${at}, ${kindOf(kind)}, has no ${key}: ${NEEDED[key]}.`);
|
|
95
|
+
}
|
|
96
|
+
if (kind === 'widget' && !sources.some((file) => file.endsWith('.swift'))) {
|
|
97
|
+
throw new Error(`crossbind: ${at} is a widget, whose SwiftUI needs a Swift source among its sources.`);
|
|
98
|
+
}
|
|
99
|
+
if (target.principalClass !== undefined && (typeof target.principalClass !== 'string' || !CLASS.test(target.principalClass))) {
|
|
100
|
+
throw new Error(`crossbind: ${at}.principalClass ${JSON.stringify(target.principalClass)} is no class name.`);
|
|
101
|
+
}
|
|
102
|
+
if (target.activation !== undefined) checkedActivation(at, target.activation);
|
|
103
|
+
if (target.categories !== undefined
|
|
104
|
+
&& (!Array.isArray(target.categories) || target.categories.length === 0 || !target.categories.every((id) => typeof id === 'string' && id !== ''))) {
|
|
105
|
+
throw new Error(`crossbind: ${at}.categories is no list of the notification categories the target shows.`);
|
|
106
|
+
}
|
|
107
|
+
if (target.extensionPoint !== undefined && (typeof target.extensionPoint !== 'string' || !EXTENSION_POINT.test(target.extensionPoint))) {
|
|
108
|
+
throw new Error(`crossbind: ${at}.extensionPoint ${JSON.stringify(target.extensionPoint)} is no extension point, as com.apple.keyboard-service is.`);
|
|
109
|
+
}
|
|
110
|
+
if (EXTENSION_KIT_POINTS.includes(target.extensionPoint)) {
|
|
111
|
+
throw new Error(`crossbind: ${at}.extensionPoint ${target.extensionPoint} is ExtensionKit's, whose extensions crossbind makes none of yet.`);
|
|
112
|
+
}
|
|
113
|
+
// The attributes of an option its kind takes, which an other target gives itself.
|
|
114
|
+
const written = Object.keys(target.attributes ?? {}).find((key) => Object.hasOwn(WRITTEN_ATTRIBUTES, key) && KINDS[kind].needs.includes(WRITTEN_ATTRIBUTES[key]));
|
|
115
|
+
if (written) throw new Error(`crossbind: ${at}.attributes gives ${written}, which crossbind writes from the target's ${WRITTEN_ATTRIBUTES[written]}.`);
|
|
116
|
+
const own = Object.keys(target.plist ?? {}).find((key) => WRITTEN_KEYS.includes(key));
|
|
117
|
+
if (own) throw new Error(`crossbind: ${at}.plist gives ${own}, which crossbind writes for each target.`);
|
|
118
|
+
const files = ['Info.plist', ...(target.privacy ? ['PrivacyInfo.xcprivacy'] : [])];
|
|
119
|
+
const resource = (target.resources ?? []).find((file) => files.includes(file.split('/').at(-1)));
|
|
120
|
+
if (resource) throw new Error(`crossbind: ${at}.resources names ${resource}, a file crossbind writes into the target's bundle.`);
|
|
121
|
+
const version = target.deploymentTarget;
|
|
122
|
+
if (version !== undefined && (typeof version !== 'string' || !VERSION.test(version))) {
|
|
123
|
+
throw new Error(`crossbind: ${at}.deploymentTarget ${JSON.stringify(version)} is no iOS version, as '17.0' is.`);
|
|
124
|
+
}
|
|
125
|
+
if (version !== undefined && isBelow(version, IOS_DEPLOYMENT_TARGET)) {
|
|
126
|
+
throw new Error(`crossbind: ${at}.deploymentTarget ${version} is below the app's iOS ${IOS_DEPLOYMENT_TARGET}, which each of its targets runs on as well.`);
|
|
127
|
+
}
|
|
128
|
+
if (target.appGroup !== undefined && typeof target.appGroup !== 'boolean') throw new Error(`crossbind: ${at}.appGroup is no boolean.`);
|
|
129
|
+
if (target.displayName !== undefined && (typeof target.displayName !== 'string' || target.displayName.trim() === '')) {
|
|
130
|
+
throw new Error(`crossbind: ${at}.displayName is no name the system shows.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// The Swift module a target's sources make, whose name takes no dash.
|
|
135
|
+
export const targetModule = (name) => name.replace(/-/g, '_');
|
|
136
|
+
|
|
137
|
+
// A target's Info.plist, as crossbind writes it, with the target's own keys: its bundle, by Xcode's settings, its
|
|
138
|
+
// extension point, principal class and attributes, and the App Group it asks for. The principal class of a target
|
|
139
|
+
// without Objective-C is Swift's, in its module, as Xcode's templates name it; one beside Objective-C goes by the name its
|
|
140
|
+
// runtime knows.
|
|
141
|
+
export function targetPlist(target, label, appGroup) {
|
|
142
|
+
const hasObjectiveC = target.sources.some(isObjectiveC);
|
|
143
|
+
const attributes = {
|
|
144
|
+
...(target.kind === 'notification-content' ? { UNNotificationExtensionInitialContentSizeRatio: 1 } : {}),
|
|
145
|
+
...target.attributes,
|
|
146
|
+
...(target.activation ? { NSExtensionActivationRule: Object.fromEntries(Object.entries(target.activation).map(([key, value]) => [ACTIVATION[key], value])) } : {}),
|
|
147
|
+
...(target.categories ? { UNNotificationExtensionCategory: target.categories.length === 1 ? target.categories[0] : target.categories } : {}),
|
|
148
|
+
};
|
|
149
|
+
const principal = target.principalClass && (hasObjectiveC ? target.principalClass : `$(PRODUCT_MODULE_NAME).${target.principalClass}`);
|
|
150
|
+
return {
|
|
151
|
+
CFBundleDevelopmentRegion: 'en',
|
|
152
|
+
CFBundleDisplayName: target.displayName ?? label,
|
|
153
|
+
CFBundleExecutable: '$(EXECUTABLE_NAME)',
|
|
154
|
+
CFBundleIdentifier: '$(PRODUCT_BUNDLE_IDENTIFIER)',
|
|
155
|
+
CFBundleInfoDictionaryVersion: '6.0',
|
|
156
|
+
CFBundleName: '$(PRODUCT_NAME)',
|
|
157
|
+
CFBundlePackageType: '$(PRODUCT_BUNDLE_PACKAGE_TYPE)',
|
|
158
|
+
CFBundleShortVersionString: '$(MARKETING_VERSION)',
|
|
159
|
+
CFBundleVersion: '$(CURRENT_PROJECT_VERSION)',
|
|
160
|
+
NSExtension: {
|
|
161
|
+
NSExtensionPointIdentifier: target.extensionPoint ?? KINDS[target.kind].point,
|
|
162
|
+
...(principal ? { NSExtensionPrincipalClass: principal } : {}),
|
|
163
|
+
...(Object.keys(attributes).length > 0 ? { NSExtensionAttributes: attributes } : {}),
|
|
164
|
+
},
|
|
165
|
+
...(target.appGroup ? { [APP_GROUP_KEY]: appGroup } : {}),
|
|
166
|
+
...target.plist,
|
|
167
|
+
};
|
|
168
|
+
}
|