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
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
// The first link a file or folder holds, by its path inside root, or none: an app carries no link, which could reach any
|
|
5
|
+
// file of the machine it is built on.
|
|
6
|
+
export function linkIn(full, root) {
|
|
7
|
+
const stat = fs.lstatSync(full);
|
|
8
|
+
if (stat.isSymbolicLink()) return path.relative(root, full);
|
|
9
|
+
if (!stat.isDirectory()) return null;
|
|
10
|
+
for (const entry of fs.readdirSync(full)) {
|
|
11
|
+
const found = linkIn(path.join(full, entry), root);
|
|
12
|
+
if (found) return found;
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Whether a path leads out of root, as a relative one of .. does.
|
|
18
|
+
export const isOutside = (relative) => relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
|
|
19
|
+
|
|
20
|
+
const LINKED = 'an app carries no link, which could reach any file of the machine it is built on';
|
|
21
|
+
|
|
22
|
+
// A file or folder a list of the files an app carries names, relative to root as it is written there, which whose names:
|
|
23
|
+
// inside root, as the system reaches it through any link on the way, and holding no link.
|
|
24
|
+
export function carriedPath(where, root, file, whose) {
|
|
25
|
+
const full = path.resolve(root, String(file));
|
|
26
|
+
const relative = path.relative(root, full);
|
|
27
|
+
if (typeof file !== 'string' || path.isAbsolute(file) || isOutside(relative) || relative === '') {
|
|
28
|
+
throw new Error(`crossbind: ${where} names ${JSON.stringify(file)}, which is no path inside the ${whose} folder.`);
|
|
29
|
+
}
|
|
30
|
+
if (!fs.existsSync(full)) throw new Error(`crossbind: ${where} names ${file}, which is no file or folder of the ${whose}.`);
|
|
31
|
+
const real = path.relative(fs.realpathSync(root), fs.realpathSync(full));
|
|
32
|
+
if (isOutside(real) || real === '') throw new Error(`crossbind: ${where} names ${file}, which a link leads out of the ${whose} folder: ${LINKED}.`);
|
|
33
|
+
const link = linkIn(full, root);
|
|
34
|
+
if (link) throw new Error(`crossbind: ${where} names ${file}, which holds the link ${link}: ${LINKED}.`);
|
|
35
|
+
return relative;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Each file the lists name, those in their folders too, by its full path: what a build fingerprints by its size and
|
|
39
|
+
// time, as a large model would take long to read at each build.
|
|
40
|
+
export function carriedFilesOf(extensions, app) {
|
|
41
|
+
const filesOf = (full) => (fs.statSync(full).isDirectory()
|
|
42
|
+
? fs.readdirSync(full).flatMap((entry) => filesOf(path.join(full, entry)))
|
|
43
|
+
: [full]);
|
|
44
|
+
return [...extensions.flatMap(({ root, paths }) => paths.map((file) => path.join(root, file))), ...app.paths.map((file) => path.join(app.root, file))]
|
|
45
|
+
.flatMap(filesOf);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// What an app carries beside its page: each extension's files in ext/<name>/ and the app's own in files/, each in the
|
|
49
|
+
// place it had in its folder, as clones where the file system makes them, as each part of a phone's app copies them.
|
|
50
|
+
export function copyCarriedFiles(resources, extensions, app) {
|
|
51
|
+
const copy = (root, file, to) => {
|
|
52
|
+
const target = path.join(to, file);
|
|
53
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
54
|
+
fs.cpSync(path.join(root, file), target, { recursive: true, mode: fs.constants.COPYFILE_FICLONE });
|
|
55
|
+
};
|
|
56
|
+
extensions.forEach(({ name, root, paths }) => paths.forEach((file) => copy(root, file, path.join(resources, 'ext', name))));
|
|
57
|
+
app.paths.forEach((file) => copy(app.root, file, path.join(resources, 'files')));
|
|
58
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// What opens an app beside its schemes' links: the files of its types, on iOS and Android the https links of its sites
|
|
2
|
+
// and the quick actions of its icon, and on iOS and macOS the user activities of its types, each with the extension that
|
|
3
|
+
// declared it, whose native code gets them, or null for the app's own, which the host hands the page.
|
|
4
|
+
|
|
5
|
+
const FILE_TYPE_KEYS = ['name', 'extensions', 'mimeTypes', 'role', 'contentTypes'];
|
|
6
|
+
const ROLES = ['editor', 'viewer'];
|
|
7
|
+
const EXTENSION = /^[a-z0-9][a-z0-9_+-]*$/;
|
|
8
|
+
// Of what an Android manifest's attribute and a C++ string literal hold as they are.
|
|
9
|
+
const MIME_TYPE = /^[a-z0-9][a-z0-9!#$^_.+-]*\/[a-z0-9][a-z0-9!#$^_.+-]*$/;
|
|
10
|
+
const CONTENT_TYPE = /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+$/;
|
|
11
|
+
// A domain of two labels at least, the last of letters or punycode, maybe with *. for its subdomains, then maybe the
|
|
12
|
+
// path its links start with, of the characters a URL's path takes as they are, but those a manifest's attribute or the
|
|
13
|
+
// host's C++ string literal escapes, and %, as Android and the host match a link's decoded path.
|
|
14
|
+
const LINK = /^(\*\.)?([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+([a-z]{2,}|xn--[a-z0-9-]*[a-z0-9])(\/[A-Za-z0-9\-._~!$()+,;=:@/]*)?$/;
|
|
15
|
+
|
|
16
|
+
const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
17
|
+
const ownerName = (extension, key) => (extension === null ? `app.${key}` : `the extension ${extension}`);
|
|
18
|
+
|
|
19
|
+
function checkedList(at, values, pattern, what, isRequired) {
|
|
20
|
+
if (!Array.isArray(values)) throw new Error(`crossbind: ${at} ${JSON.stringify(values)} is no list of ${what}.`);
|
|
21
|
+
if (isRequired && values.length === 0) throw new Error(`crossbind: ${at} lists none, where it takes ${what}.`);
|
|
22
|
+
for (const value of values) {
|
|
23
|
+
if (typeof value !== 'string' || !pattern.test(value)) throw new Error(`crossbind: ${at} takes ${what}; ${JSON.stringify(value)} is none.`);
|
|
24
|
+
}
|
|
25
|
+
return values;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// The types of files an app opens: each with its name, which the system shows its files' kind by, the extensions of its
|
|
29
|
+
// files, the MIME types Android and Linux match them by, its role, an editor's unless a viewer's, and the uniform type
|
|
30
|
+
// identifiers Apple's platforms know a type by that they have of their own, as CSV's.
|
|
31
|
+
export function checkedFileTypes(types, where) {
|
|
32
|
+
if (!Array.isArray(types)) {
|
|
33
|
+
throw new Error(`crossbind: ${where} ${JSON.stringify(types)} is no list of file types, as [{ name: 'GeoJSON', extensions: ['geojson'] }] is.`);
|
|
34
|
+
}
|
|
35
|
+
return types.map((type, index) => {
|
|
36
|
+
const at = `${where}[${index}]`;
|
|
37
|
+
if (!isPlainObject(type)) throw new Error(`crossbind: ${at} is no file type, as { name: 'GeoJSON', extensions: ['geojson'] } is.`);
|
|
38
|
+
const unknown = Object.keys(type).filter((key) => !FILE_TYPE_KEYS.includes(key));
|
|
39
|
+
if (unknown.length > 0) throw new Error(`crossbind: ${at} takes ${FILE_TYPE_KEYS.join(', ')}; ${unknown.join(', ')} is none of them.`);
|
|
40
|
+
if (typeof type.name !== 'string' || type.name.trim() === '') throw new Error(`crossbind: ${at} has no name, which the system shows its files' kind by.`);
|
|
41
|
+
// A Linux desktop entry and a Windows setup program hold it on one line.
|
|
42
|
+
if ([...type.name].some((character) => character.charCodeAt(0) < 0x20 || character.charCodeAt(0) === 0x7f)) throw new Error(`crossbind: ${at}.name ${JSON.stringify(type.name)} holds a control character, as a line break; name it on one line.`);
|
|
43
|
+
const role = type.role ?? 'editor';
|
|
44
|
+
if (!ROLES.includes(role)) throw new Error(`crossbind: ${at}.role is 'editor' or 'viewer'; ${JSON.stringify(type.role)} is neither.`);
|
|
45
|
+
return {
|
|
46
|
+
name: type.name,
|
|
47
|
+
extensions: checkedList(`${at}.extensions`, type.extensions, EXTENSION, "lowercase letters and digits without the dot, as 'geojson' is", true),
|
|
48
|
+
mimeTypes: checkedList(`${at}.mimeTypes`, type.mimeTypes ?? [], MIME_TYPE, "MIME types, as 'application/geo+json' is", false),
|
|
49
|
+
role,
|
|
50
|
+
contentTypes: checkedList(`${at}.contentTypes`, type.contentTypes ?? [], CONTENT_TYPE, "uniform type identifiers, as 'public.comma-separated-values-text' is", false),
|
|
51
|
+
};
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// The types of files that open the app, app.fileTypes's, then its extensions', each with the extension that declared it,
|
|
56
|
+
// or null. One owner of an extension's files alone: two stop the build, naming both.
|
|
57
|
+
export function fileTypeOwners(config) {
|
|
58
|
+
const owned = [
|
|
59
|
+
...checkedFileTypes(config.app.fileTypes ?? [], 'app.fileTypes').map((type) => ({ ...type, extension: null })),
|
|
60
|
+
...(config.extensions ?? []).flatMap(({ name, app }) => (app.fileTypes ?? []).map((type) => ({ ...type, extension: name }))),
|
|
61
|
+
];
|
|
62
|
+
const owners = new Map();
|
|
63
|
+
for (const { extensions, extension } of owned) {
|
|
64
|
+
for (const ending of extensions) {
|
|
65
|
+
if (!owners.has(ending)) {
|
|
66
|
+
owners.set(ending, extension);
|
|
67
|
+
} else if (owners.get(ending) === extension) {
|
|
68
|
+
throw new Error(`crossbind: ${ownerName(extension, 'fileTypes')} lists the files .${ending} twice.`);
|
|
69
|
+
} else {
|
|
70
|
+
throw new Error(`crossbind: ${ownerName(owners.get(ending), 'fileTypes')} and ${ownerName(extension, 'fileTypes')} both claim the files .${ending}, which one alone opens.`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return owned;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const ROLE_NAMES = { editor: 'Editor', viewer: 'Viewer' };
|
|
78
|
+
|
|
79
|
+
// A macOS app's document types: each by its extensions, unless its content types name it.
|
|
80
|
+
export function macDocumentTypes(fileTypes) {
|
|
81
|
+
return fileTypes.map(({ name, extensions, mimeTypes, role, contentTypes }) => ({
|
|
82
|
+
CFBundleTypeName: name,
|
|
83
|
+
CFBundleTypeRole: ROLE_NAMES[role],
|
|
84
|
+
...(contentTypes.length > 0 ? { LSItemContentTypes: contentTypes } : { CFBundleTypeExtensions: extensions }),
|
|
85
|
+
...(mimeTypes.length > 0 ? { CFBundleTypeMIMETypes: mimeTypes } : {}),
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// An iOS app's document types, which iOS knows by type identifiers alone: Apple's that a type's content types name, else
|
|
90
|
+
// one the app declares from the type's extensions and MIME types, named after its bundle id. The app opens a document
|
|
91
|
+
// where it is.
|
|
92
|
+
export function iosDocumentTypes(fileTypes, bundleId) {
|
|
93
|
+
if (fileTypes.length === 0) return {};
|
|
94
|
+
// A type identifier takes letters, digits, - and . alone.
|
|
95
|
+
const ownIdentifier = (type) => `${bundleId}.${type.extensions[0].replace(/[^A-Za-z0-9-]/g, '-')}`;
|
|
96
|
+
return {
|
|
97
|
+
CFBundleDocumentTypes: fileTypes.map((type) => ({
|
|
98
|
+
CFBundleTypeName: type.name,
|
|
99
|
+
CFBundleTypeRole: ROLE_NAMES[type.role],
|
|
100
|
+
LSItemContentTypes: type.contentTypes.length > 0 ? type.contentTypes : [ownIdentifier(type)],
|
|
101
|
+
})),
|
|
102
|
+
UTImportedTypeDeclarations: fileTypes.filter((type) => type.contentTypes.length === 0).map((type) => ({
|
|
103
|
+
UTTypeIdentifier: ownIdentifier(type),
|
|
104
|
+
UTTypeDescription: type.name,
|
|
105
|
+
UTTypeConformsTo: ['public.data'],
|
|
106
|
+
UTTypeTagSpecification: {
|
|
107
|
+
'public.filename-extension': type.extensions,
|
|
108
|
+
...(type.mimeTypes.length > 0 ? { 'public.mime-type': type.mimeTypes } : {}),
|
|
109
|
+
},
|
|
110
|
+
})),
|
|
111
|
+
LSSupportsOpeningDocumentsInPlace: true,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// The https links of a list's sites, which open the app on iOS and Android: each a domain, lowercase, maybe with *. for
|
|
116
|
+
// its subdomains, then maybe the path its links start with, as example.com/invite is.
|
|
117
|
+
export function checkedLinks(links, where) {
|
|
118
|
+
if (!Array.isArray(links)) throw new Error(`crossbind: ${where} ${JSON.stringify(links)} is no list of links, as ['example.com/invite'] is.`);
|
|
119
|
+
for (const link of links) {
|
|
120
|
+
if (typeof link !== 'string' || !LINK.test(link)) {
|
|
121
|
+
const scheme = typeof link === 'string' ? /^(https?):\/\//.exec(link)?.[1] : undefined;
|
|
122
|
+
throw new Error(`crossbind: ${where} takes a site's domain, maybe with *. for its subdomains, then maybe the path its links start with, as example.com/invite is${scheme ? `, without its ${scheme}://` : ''}; ${JSON.stringify(link)} is none.`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const twice = links.find((link, index) => links.indexOf(link) !== index);
|
|
126
|
+
if (twice) throw new Error(`crossbind: ${where} lists ${twice} twice.`);
|
|
127
|
+
return links;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The type of a user activity or a quick action: a reverse-DNS name or an intent's class, of what an Info.plist's
|
|
131
|
+
// string, an Android resource's attribute and the host's C++ string literal hold as they are.
|
|
132
|
+
const TYPE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
133
|
+
// A web page's activity is a site's link.
|
|
134
|
+
const BROWSING_WEB = 'NSUserActivityTypeBrowsingWeb';
|
|
135
|
+
|
|
136
|
+
// The types of the user activities an iOS or macOS app continues, as Handoff from another of its user's devices does,
|
|
137
|
+
// each once.
|
|
138
|
+
export function checkedActivityTypes(types, where) {
|
|
139
|
+
checkedList(where, types, TYPE_NAME, "user activity types of letters, digits, ., _ and -, as 'com.example.app.edit' is", false);
|
|
140
|
+
if (types.includes(BROWSING_WEB)) throw new Error(`crossbind: ${where} cannot take ${BROWSING_WEB}, a web page's, which app.links takes by its site.`);
|
|
141
|
+
const twice = types.find((type, index) => types.indexOf(type) !== index);
|
|
142
|
+
if (twice) throw new Error(`crossbind: ${where} lists ${twice} twice.`);
|
|
143
|
+
return types;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// The types of the user activities the app continues, app.activityTypes's, then its extensions', each with the extension
|
|
147
|
+
// that declared it, or null, whose activities the host's activities hands the page. Two owners of one stop the build,
|
|
148
|
+
// naming both.
|
|
149
|
+
export function activityTypeOwners(config) {
|
|
150
|
+
const owned = [
|
|
151
|
+
...checkedActivityTypes(config.app.activityTypes ?? [], 'app.activityTypes').map((type) => ({ type, extension: null })),
|
|
152
|
+
...(config.extensions ?? []).flatMap(({ name, app }) => (app.activityTypes ?? []).map((type) => ({ type, extension: name }))),
|
|
153
|
+
];
|
|
154
|
+
owned.forEach(({ type, extension }, index) => {
|
|
155
|
+
const first = owned.findIndex((other) => other.type === type);
|
|
156
|
+
if (first !== index) {
|
|
157
|
+
throw new Error(`crossbind: ${ownerName(owned[first].extension, 'activityTypes')} and ${ownerName(extension, 'activityTypes')} both claim the user activities ${type}, which one alone continues.`);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
return owned;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const SHORTCUT_KEYS = ['type', 'title', 'subtitle', 'icon'];
|
|
164
|
+
// iOS's own icons of a quick action, UIApplicationShortcutIconType's names without its prefix.
|
|
165
|
+
const SHORTCUT_ICONS = [
|
|
166
|
+
'add', 'alarm', 'audio', 'bookmark', 'capturePhoto', 'captureVideo', 'cloud', 'compose', 'confirmation', 'contact', 'date', 'favorite', 'home',
|
|
167
|
+
'invitation', 'location', 'love', 'mail', 'markLocation', 'message', 'pause', 'play', 'prohibit', 'search', 'share', 'shuffle', 'task',
|
|
168
|
+
'taskCompleted', 'time', 'update',
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
// Text the system shows on one line, or null for none where it may have none.
|
|
172
|
+
function checkedLine(at, value, isRequired) {
|
|
173
|
+
if (value === undefined && !isRequired) return null;
|
|
174
|
+
if (typeof value !== 'string' || value.trim() === '') throw new Error(`crossbind: ${at} is no text, which the system shows.`);
|
|
175
|
+
// XML holds neither \uFFFE nor \uFFFF.
|
|
176
|
+
if (/[\p{Cc}\uFFFE\uFFFF]/u.test(value)) {
|
|
177
|
+
throw new Error(`crossbind: ${at} ${JSON.stringify(value)} holds a control character, as a line break; give it on one line.`);
|
|
178
|
+
}
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// The quick actions of an iOS or Android app's icon: each by its type, with its title, and on iOS a subtitle and one of
|
|
183
|
+
// its own icons, as compose is.
|
|
184
|
+
export function checkedShortcutItems(items, where) {
|
|
185
|
+
if (!Array.isArray(items)) throw new Error(`crossbind: ${where} ${JSON.stringify(items)} is no list of quick actions, as [{ type: 'new-note', title: 'New note' }] is.`);
|
|
186
|
+
const checked = items.map((item, index) => {
|
|
187
|
+
const at = `${where}[${index}]`;
|
|
188
|
+
if (!isPlainObject(item)) throw new Error(`crossbind: ${at} is no quick action, as { type: 'new-note', title: 'New note' } is.`);
|
|
189
|
+
const unknown = Object.keys(item).filter((key) => !SHORTCUT_KEYS.includes(key));
|
|
190
|
+
if (unknown.length > 0) throw new Error(`crossbind: ${at} takes ${SHORTCUT_KEYS.join(', ')}; ${unknown.join(', ')} is none of them.`);
|
|
191
|
+
if (typeof item.type !== 'string' || !TYPE_NAME.test(item.type)) {
|
|
192
|
+
throw new Error(`crossbind: ${at}.type takes letters, digits, ., _ and -, as 'new-note' does; ${JSON.stringify(item.type)} is none.`);
|
|
193
|
+
}
|
|
194
|
+
if (item.icon !== undefined && item.icon !== null && !SHORTCUT_ICONS.includes(item.icon)) {
|
|
195
|
+
throw new Error(`crossbind: ${at}.icon is one of iOS's own icons, ${SHORTCUT_ICONS.join(', ')}; ${JSON.stringify(item.icon)} is none.`);
|
|
196
|
+
}
|
|
197
|
+
const subtitle = item.subtitle === null ? null : checkedLine(`${at}.subtitle`, item.subtitle, false);
|
|
198
|
+
return { type: item.type, title: checkedLine(`${at}.title`, item.title, true), subtitle, icon: item.icon ?? null };
|
|
199
|
+
});
|
|
200
|
+
const twice = checked.find(({ type }, index) => checked.findIndex((other) => other.type === type) !== index);
|
|
201
|
+
if (twice) throw new Error(`crossbind: ${where} lists the quick action ${twice.type} twice.`);
|
|
202
|
+
return checked;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// The quick actions of the app's icon, app.shortcutItems's, then its extensions', each with the extension that declared
|
|
206
|
+
// it, or null, whose taps the host's shortcuts hands the page. Two owners of one type stop the build, naming both.
|
|
207
|
+
export function shortcutItemOwners(config) {
|
|
208
|
+
const owned = [
|
|
209
|
+
...checkedShortcutItems(config.app.shortcutItems ?? [], 'app.shortcutItems').map((item) => ({ ...item, extension: null })),
|
|
210
|
+
...(config.extensions ?? []).flatMap(({ name, app }) => (app.shortcutItems ?? []).map((item) => ({ ...item, extension: name }))),
|
|
211
|
+
];
|
|
212
|
+
owned.forEach(({ type, extension }, index) => {
|
|
213
|
+
const first = owned.findIndex((other) => other.type === type);
|
|
214
|
+
if (first !== index) {
|
|
215
|
+
throw new Error(`crossbind: ${ownerName(owned[first].extension, 'shortcutItems')} and ${ownerName(extension, 'shortcutItems')} both claim the quick action ${type}, whose taps one alone gets.`);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
return owned;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// An iOS app's quick actions, as its Info.plist lists them.
|
|
222
|
+
export const iosShortcutItems = (items) => items.map(({ type, title, subtitle, icon }) => ({
|
|
223
|
+
UIApplicationShortcutItemType: type,
|
|
224
|
+
UIApplicationShortcutItemTitle: title,
|
|
225
|
+
...(subtitle ? { UIApplicationShortcutItemSubtitle: subtitle } : {}),
|
|
226
|
+
...(icon ? { UIApplicationShortcutItemIconType: `UIApplicationShortcutIconType${icon[0].toUpperCase()}${icon.slice(1)}` } : {}),
|
|
227
|
+
}));
|
|
228
|
+
|
|
229
|
+
// A link's domain and the path its links start with, empty for all of the site's.
|
|
230
|
+
export function appLinkParts(link) {
|
|
231
|
+
const slash = link.indexOf('/');
|
|
232
|
+
return slash < 0 ? { host: link, path: '' } : { host: link.slice(0, slash), path: link.slice(slash) };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// The links that open the app, app.links's, then its extensions', each with the extension that declared it, or null,
|
|
236
|
+
// whose links the host's links hands the page. Two owners of one stop the build, naming both.
|
|
237
|
+
export function linkOwners(config) {
|
|
238
|
+
const owned = [
|
|
239
|
+
...checkedLinks(config.app.links ?? [], 'app.links').map((link) => ({ link, extension: null })),
|
|
240
|
+
...(config.extensions ?? []).flatMap(({ name, app }) => (app.links ?? []).map((link) => ({ link, extension: name }))),
|
|
241
|
+
];
|
|
242
|
+
owned.forEach(({ link, extension }, index) => {
|
|
243
|
+
const first = owned.findIndex((other) => other.link === link);
|
|
244
|
+
if (first !== index) {
|
|
245
|
+
throw new Error(`crossbind: ${ownerName(owned[first].extension, 'links')} and ${ownerName(extension, 'links')} both claim the links of ${link}, which one alone gets.`);
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
return owned;
|
|
249
|
+
}
|