craft-native 0.0.90 → 0.0.92
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/dist/android/src/index.d.ts +36 -1
- package/dist/android/src/index.js +332 -44
- package/dist/android/src/promise-runtime.d.ts +3 -0
- package/dist/android/templates/CraftBridge.kt.template +2167 -1177
- package/dist/android/templates/CraftHealthConnect.kt.template +45 -6
- package/dist/android/templates/CraftHealthConnectStub.kt.template +7 -1
- package/dist/android/templates/CraftNative.kt.template +2250 -0
- package/dist/android/templates/LocationRecordingService.kt.template +34 -9
- package/dist/android/templates/MainActivity.kt.template +42 -8
- package/dist/android/templates/proguard-rules.pro.template +4 -1
- package/dist/android/templates/test-bridges.html +10 -33
- package/dist/api/index.d.ts +1 -1
- package/dist/api/ios-advanced.d.ts +8 -5
- package/dist/api/live-activity-handle.d.ts +6 -0
- package/dist/api/mobile.d.ts +25 -7
- package/dist/api/window.d.ts +2 -0
- package/dist/cli.js +452 -129
- package/dist/index.cjs +77 -22
- package/dist/index.js +77 -22
- package/dist/ios/src/index.d.ts +1 -1
- package/dist/ios/src/index.js +23 -5
- package/dist/ios/templates/CraftApp.swift +706 -92
- package/dist/ios/templates/CraftWatchApp.swift.template +8 -0
- package/dist/ios/templates/WatchApp.Info.plist.template +2 -0
- package/dist/ios/templates/WatchExtension.Info.plist.template +34 -0
- package/dist/ios/templates/project.yml.template +10 -4
- package/dist/mobile.js +52 -21
- package/dist/scaffold-version.d.ts +5 -0
- package/package.json +1 -1
- package/dist/android/templates/CraftBridgeExtensions.kt.template +0 -383
- package/dist/android/templates/CraftWidgetProvider.kt.template +0 -246
|
@@ -37,6 +37,14 @@ export interface InitOptions {
|
|
|
37
37
|
packageName?: string;
|
|
38
38
|
output: string;
|
|
39
39
|
config?: Partial<CraftAndroidConfig>;
|
|
40
|
+
/**
|
|
41
|
+
* Where to find the Zig runtime's `<abi>/libcraft.so`.
|
|
42
|
+
*
|
|
43
|
+
* `null` means no runtime regardless of the environment; `undefined` falls
|
|
44
|
+
* through to `CRAFT_ANDROID_RUNTIME`. Same shape as the iOS builder's
|
|
45
|
+
* `runtimeDir`.
|
|
46
|
+
*/
|
|
47
|
+
runtimeDir?: string | null;
|
|
40
48
|
}
|
|
41
49
|
export interface BuildOptions {
|
|
42
50
|
htmlPath?: string;
|
|
@@ -44,6 +52,8 @@ export interface BuildOptions {
|
|
|
44
52
|
output: string;
|
|
45
53
|
release?: boolean;
|
|
46
54
|
compile?: boolean;
|
|
55
|
+
/** Same meaning as `InitOptions.runtimeDir`; refreshes what init installed. */
|
|
56
|
+
runtimeDir?: string | null;
|
|
47
57
|
}
|
|
48
58
|
export interface OpenOptions {
|
|
49
59
|
output: string;
|
|
@@ -56,8 +66,33 @@ export declare function syncAndroidWebAssets(source: string, output: string): vo
|
|
|
56
66
|
export declare function renderAndroidPermissions(config: CraftAndroidConfig): string;
|
|
57
67
|
export declare function renderAndroidDeepLinks(config: CraftAndroidConfig): string;
|
|
58
68
|
/**
|
|
59
|
-
*
|
|
69
|
+
* Where the runtime directory comes from when the caller does not say.
|
|
70
|
+
*
|
|
71
|
+
* The same shape as `CRAFT_BIN` and the iOS builder's `CRAFT_IOS_RUNTIME`: an
|
|
72
|
+
* explicit override for the monorepo dev loop, not a lookup path. Shipping the
|
|
73
|
+
* runtime to real apps means putting these libraries in the pantry package
|
|
74
|
+
* beside the `craft` binary, which is a distribution decision this function
|
|
75
|
+
* does not make.
|
|
76
|
+
*/
|
|
77
|
+
export declare function resolveRuntimeDir(override?: string | null): string | null;
|
|
78
|
+
/**
|
|
79
|
+
* Copy the Zig runtime into the generated project as
|
|
80
|
+
* `app/src/main/jniLibs/<abi>/libcraft.so`.
|
|
81
|
+
*
|
|
82
|
+
* That path is AGP's default `jniLibs.srcDirs`, so nothing in the Gradle
|
|
83
|
+
* templates has to know about it — the library is packaged into the APK and
|
|
84
|
+
* `System.loadLibrary("craft")` finds it because the file is named for the
|
|
85
|
+
* `craft` it asks for.
|
|
86
|
+
*
|
|
87
|
+
* Until this existed, nothing put the library anywhere. `CraftNative` caught
|
|
88
|
+
* the `UnsatisfiedLinkError`, set `isAvailable = false`, and every action fell
|
|
89
|
+
* through to the Kotlin shim — by design, so an app with no runtime still
|
|
90
|
+
* works, which is also why nobody noticed that *every* generated app was in
|
|
91
|
+
* that state and the whole Android half of the Zig bridge had never run.
|
|
92
|
+
*
|
|
93
|
+
* Returns true when a runtime was installed.
|
|
60
94
|
*/
|
|
95
|
+
export declare function installRuntime(output: string, runtimeDir: string): boolean;
|
|
61
96
|
export declare function init(options: InitOptions): Promise<void>;
|
|
62
97
|
/**
|
|
63
98
|
* Build Android project
|
|
@@ -1,9 +1,115 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../android/src/index.ts
|
|
3
3
|
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
4
|
-
import { dirname, join, resolve } from "path";
|
|
4
|
+
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
5
5
|
var {$ } = globalThis.Bun;
|
|
6
|
+
|
|
7
|
+
// ../android/src/promise-runtime.ts
|
|
8
|
+
var ANDROID_PROMISE_RUNTIME = `
|
|
9
|
+
if (window.__craftRejectPendingPromises) {
|
|
10
|
+
window.__craftRejectPendingPromises('Android bridge reinitialized');
|
|
11
|
+
}
|
|
12
|
+
if (window.__craftRejectPermissionRequests) {
|
|
13
|
+
window.__craftRejectPermissionRequests('Android bridge reinitialized');
|
|
14
|
+
}
|
|
15
|
+
window.__craftPromiseRuntimeClosed = false;
|
|
16
|
+
window.__craftPendingPromises = Object.create(null);
|
|
17
|
+
window.__craftPromise = function(channel, resolveName, rejectName, invoke, timeoutMs, timeoutError) {
|
|
18
|
+
if (window.__craftPromiseRuntimeClosed) {
|
|
19
|
+
return Promise.reject(new Error('Android bridge is closed'));
|
|
20
|
+
}
|
|
21
|
+
if (window.__craftPendingPromises[channel]) {
|
|
22
|
+
return Promise.reject(new Error('A '.concat(channel, ' request is already in progress')));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return new Promise(function(resolve, reject) {
|
|
26
|
+
var entry = {settled: false, timer: null, settle: null};
|
|
27
|
+
var resolveCallback;
|
|
28
|
+
var rejectCallback;
|
|
29
|
+
|
|
30
|
+
entry.settle = function(succeeded, value) {
|
|
31
|
+
if (entry.settled || window.__craftPendingPromises[channel] !== entry) return;
|
|
32
|
+
entry.settled = true;
|
|
33
|
+
if (entry.timer !== null) clearTimeout(entry.timer);
|
|
34
|
+
if (window[resolveName] === resolveCallback) window[resolveName] = null;
|
|
35
|
+
if (window[rejectName] === rejectCallback) window[rejectName] = null;
|
|
36
|
+
delete window.__craftPendingPromises[channel];
|
|
37
|
+
if (succeeded) resolve(value);
|
|
38
|
+
else reject(value);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
resolveCallback = function(value) {
|
|
42
|
+
entry.settle(true, value);
|
|
43
|
+
};
|
|
44
|
+
rejectCallback = function(error) {
|
|
45
|
+
entry.settle(false, error);
|
|
46
|
+
};
|
|
47
|
+
window[resolveName] = resolveCallback;
|
|
48
|
+
window[rejectName] = rejectCallback;
|
|
49
|
+
window.__craftPendingPromises[channel] = entry;
|
|
50
|
+
|
|
51
|
+
if (timeoutMs > 0) {
|
|
52
|
+
entry.timer = setTimeout(function() {
|
|
53
|
+
entry.settle(false, timeoutError || new Error(channel.concat(' request timed out')));
|
|
54
|
+
}, timeoutMs);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
invoke();
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
entry.settle(false, error);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
window.__craftRejectPendingPromises = function(message) {
|
|
67
|
+
window.__craftPromiseRuntimeClosed = true;
|
|
68
|
+
Object.keys(window.__craftPendingPromises).forEach(function(channel) {
|
|
69
|
+
var entry = window.__craftPendingPromises[channel];
|
|
70
|
+
if (entry) entry.settle(false, new Error(message || 'Android bridge closed'));
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
`;
|
|
74
|
+
function renderAndroidPromiseRuntime(indent = "") {
|
|
75
|
+
return ANDROID_PROMISE_RUNTIME.trim().split(`
|
|
76
|
+
`).map((line) => `${indent}${line}`).join(`
|
|
77
|
+
`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ../android/src/index.ts
|
|
6
81
|
var TEMPLATES_DIR = join(dirname(import.meta.dir), "templates");
|
|
82
|
+
var LOCAL_DEVELOPMENT_HOSTS = new Set(["localhost", "127.0.0.1", "10.0.2.2"]);
|
|
83
|
+
var KOTLIN_KEYWORDS = new Set([
|
|
84
|
+
"as",
|
|
85
|
+
"break",
|
|
86
|
+
"class",
|
|
87
|
+
"continue",
|
|
88
|
+
"do",
|
|
89
|
+
"else",
|
|
90
|
+
"false",
|
|
91
|
+
"for",
|
|
92
|
+
"fun",
|
|
93
|
+
"if",
|
|
94
|
+
"in",
|
|
95
|
+
"interface",
|
|
96
|
+
"is",
|
|
97
|
+
"null",
|
|
98
|
+
"object",
|
|
99
|
+
"package",
|
|
100
|
+
"return",
|
|
101
|
+
"super",
|
|
102
|
+
"this",
|
|
103
|
+
"throw",
|
|
104
|
+
"true",
|
|
105
|
+
"try",
|
|
106
|
+
"typealias",
|
|
107
|
+
"typeof",
|
|
108
|
+
"val",
|
|
109
|
+
"var",
|
|
110
|
+
"when",
|
|
111
|
+
"while"
|
|
112
|
+
]);
|
|
7
113
|
var DEFAULT_CONFIG = {
|
|
8
114
|
version: "1.0.0",
|
|
9
115
|
versionCode: 1,
|
|
@@ -27,16 +133,135 @@ var DEFAULT_CONFIG = {
|
|
|
27
133
|
compileSdk: 36,
|
|
28
134
|
targetSdk: 35
|
|
29
135
|
};
|
|
136
|
+
function escapeXml(value) {
|
|
137
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
138
|
+
}
|
|
139
|
+
function escapeKotlinString(value) {
|
|
140
|
+
return value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("$", "\\$").replaceAll("\r", "\\r").replaceAll(`
|
|
141
|
+
`, "\\n");
|
|
142
|
+
}
|
|
143
|
+
function generatedPackageSegment(name) {
|
|
144
|
+
const normalized = name.toLowerCase().replace(/[^a-z0-9_]/g, "");
|
|
145
|
+
if (!normalized)
|
|
146
|
+
return "app";
|
|
147
|
+
return /^[a-z_]/.test(normalized) ? normalized : `app${normalized}`;
|
|
148
|
+
}
|
|
149
|
+
function generatedGradleProjectName(name) {
|
|
150
|
+
const invalidCharacters = ["/", "\\", ":", "<", ">", '"', "?", "*", "|"];
|
|
151
|
+
const normalized = invalidCharacters.reduce((value, character) => value.replaceAll(character, "-"), name).trim();
|
|
152
|
+
return normalized || "craft-app";
|
|
153
|
+
}
|
|
154
|
+
function requireRegularFile(path, label) {
|
|
155
|
+
if (!existsSync(path))
|
|
156
|
+
throw new Error(`${label} not found: ${path}`);
|
|
157
|
+
if (!statSync(path).isFile())
|
|
158
|
+
throw new Error(`${label} must be a file: ${path}`);
|
|
159
|
+
}
|
|
160
|
+
function containsPath(parent, candidate) {
|
|
161
|
+
const relativePath = relative(parent, candidate);
|
|
162
|
+
return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
|
|
163
|
+
}
|
|
164
|
+
function validateGoogleServicesFile(path, packageName) {
|
|
165
|
+
requireRegularFile(path, "Google services file");
|
|
166
|
+
let document;
|
|
167
|
+
try {
|
|
168
|
+
document = JSON.parse(readFileSync(path, "utf8"));
|
|
169
|
+
} catch (error) {
|
|
170
|
+
throw new Error(`Google services file must contain valid JSON: ${path}`, { cause: error });
|
|
171
|
+
}
|
|
172
|
+
const matchingClient = document.client?.some((client) => {
|
|
173
|
+
return client.client_info?.android_client_info?.package_name === packageName;
|
|
174
|
+
});
|
|
175
|
+
if (!matchingClient) {
|
|
176
|
+
throw new Error(`Google services file has no client for Android package ${packageName}: ${path}`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function androidWebUrl(value, field) {
|
|
180
|
+
let url;
|
|
181
|
+
try {
|
|
182
|
+
url = new URL(value);
|
|
183
|
+
} catch {
|
|
184
|
+
throw new Error(`Invalid ${field}: ${value}`);
|
|
185
|
+
}
|
|
186
|
+
const localDevelopment = url.protocol === "http:" && LOCAL_DEVELOPMENT_HOSTS.has(url.hostname);
|
|
187
|
+
if (url.protocol !== "https:" && !localDevelopment || url.username || url.password) {
|
|
188
|
+
throw new Error(`${field} must use HTTPS or local HTTP without credentials: ${value}`);
|
|
189
|
+
}
|
|
190
|
+
return url;
|
|
191
|
+
}
|
|
192
|
+
function normalizeAndroidNetworkConfig(config) {
|
|
193
|
+
const schemes = config.urlSchemes?.map((value) => value.trim().toLowerCase()) ?? [];
|
|
194
|
+
if (schemes.some((value) => !/^[a-z][a-z0-9+.-]*$/.test(value))) {
|
|
195
|
+
throw new Error("Android deep-link schemes must be valid URI schemes");
|
|
196
|
+
}
|
|
197
|
+
config.urlSchemes = [...new Set(schemes)];
|
|
198
|
+
if (config.enableDeepLinks && config.urlSchemes.length === 0) {
|
|
199
|
+
throw new Error("Android deep links require at least one URL scheme");
|
|
200
|
+
}
|
|
201
|
+
const trustedOrigins = (config.trustedOrigins ?? []).map((value) => {
|
|
202
|
+
return androidWebUrl(value, "Android trusted origin").origin;
|
|
203
|
+
});
|
|
204
|
+
if (config.devServerURL) {
|
|
205
|
+
const devServer = androidWebUrl(config.devServerURL, "Android dev server URL");
|
|
206
|
+
config.devServerURL = devServer.toString();
|
|
207
|
+
trustedOrigins.push(devServer.origin);
|
|
208
|
+
}
|
|
209
|
+
config.trustedOrigins = [...new Set(trustedOrigins)];
|
|
210
|
+
}
|
|
211
|
+
function validateAndroidConfig(config) {
|
|
212
|
+
if (!config.appName.trim())
|
|
213
|
+
throw new Error("Android app name must not be empty");
|
|
214
|
+
const packageSegments = config.packageName.split(".");
|
|
215
|
+
if (packageSegments.length < 2 || packageSegments.some((segment) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(segment) || KOTLIN_KEYWORDS.has(segment))) {
|
|
216
|
+
throw new Error(`Invalid Android package name: ${config.packageName}`);
|
|
217
|
+
}
|
|
218
|
+
if (!/^#(?:[\dA-F]{3,4}|[\dA-F]{6}|[\dA-F]{8})$/i.test(config.backgroundColor ?? "")) {
|
|
219
|
+
throw new Error(`Invalid Android background color: ${config.backgroundColor}`);
|
|
220
|
+
}
|
|
221
|
+
for (const [name, value] of [
|
|
222
|
+
["versionCode", config.versionCode],
|
|
223
|
+
["minSdk", config.minSdk],
|
|
224
|
+
["compileSdk", config.compileSdk],
|
|
225
|
+
["targetSdk", config.targetSdk]
|
|
226
|
+
]) {
|
|
227
|
+
if (!Number.isInteger(value) || Number(value) < 1) {
|
|
228
|
+
throw new Error(`Android ${name} must be a positive integer`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (Number(config.minSdk) > Number(config.targetSdk)) {
|
|
232
|
+
throw new Error("Android minSdk must not exceed targetSdk");
|
|
233
|
+
}
|
|
234
|
+
if (Number(config.targetSdk) > Number(config.compileSdk)) {
|
|
235
|
+
throw new Error("Android targetSdk must not exceed compileSdk");
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
function writeAndroidConfig(output, config) {
|
|
239
|
+
writeFileSync(join(output, "craft.config.json"), JSON.stringify(config, null, 2));
|
|
240
|
+
const runtimeConfig = { ...config };
|
|
241
|
+
delete runtimeConfig.appIconPath;
|
|
242
|
+
delete runtimeConfig.googleServicesFile;
|
|
243
|
+
writeFileSync(join(output, "app/src/main/assets/craft.config.json"), JSON.stringify(runtimeConfig, null, 2));
|
|
244
|
+
}
|
|
30
245
|
function syncAndroidWebAssets(source, output) {
|
|
31
246
|
const sourcePath = resolve(source);
|
|
32
247
|
if (!existsSync(sourcePath))
|
|
33
248
|
throw new Error(`Web asset path not found: ${source}`);
|
|
34
|
-
const
|
|
249
|
+
const sourceStat = statSync(sourcePath);
|
|
250
|
+
if (!sourceStat.isDirectory() && !sourceStat.isFile()) {
|
|
251
|
+
throw new Error(`Web asset path must be a file or directory: ${source}`);
|
|
252
|
+
}
|
|
253
|
+
const assetsDir = resolve(output, "app/src/main/assets");
|
|
254
|
+
if (containsPath(sourcePath, assetsDir) || containsPath(assetsDir, sourcePath)) {
|
|
255
|
+
throw new Error(`Web asset source must not overlap generated asset directory: ${source}`);
|
|
256
|
+
}
|
|
257
|
+
if (sourceStat.isDirectory()) {
|
|
258
|
+
requireRegularFile(join(sourcePath, "index.html"), "Web asset directory entry point");
|
|
259
|
+
}
|
|
35
260
|
const configPath = join(assetsDir, "craft.config.json");
|
|
36
261
|
const config = existsSync(configPath) ? readFileSync(configPath) : undefined;
|
|
37
262
|
rmSync(assetsDir, { recursive: true, force: true });
|
|
38
263
|
mkdirSync(assetsDir, { recursive: true });
|
|
39
|
-
if (
|
|
264
|
+
if (sourceStat.isDirectory())
|
|
40
265
|
cpSync(sourcePath, assetsDir, { recursive: true });
|
|
41
266
|
else
|
|
42
267
|
cpSync(sourcePath, join(assetsDir, "index.html"));
|
|
@@ -93,14 +318,70 @@ function renderAndroidDeepLinks(config) {
|
|
|
93
318
|
</intent-filter>`).join(`
|
|
94
319
|
`);
|
|
95
320
|
}
|
|
321
|
+
var RUNTIME_ABIS = ["arm64-v8a", "x86_64"];
|
|
322
|
+
function resolveRuntimeDir(override) {
|
|
323
|
+
if (override === null)
|
|
324
|
+
return null;
|
|
325
|
+
const dir = override ?? process.env.CRAFT_ANDROID_RUNTIME;
|
|
326
|
+
if (!dir)
|
|
327
|
+
return null;
|
|
328
|
+
if (!existsSync(dir)) {
|
|
329
|
+
const source = override === undefined ? "CRAFT_ANDROID_RUNTIME points at" : "runtimeDir is";
|
|
330
|
+
throw new Error(`${source} ${dir}, which does not exist.`);
|
|
331
|
+
}
|
|
332
|
+
return dir;
|
|
333
|
+
}
|
|
334
|
+
function installRuntime(output, runtimeDir) {
|
|
335
|
+
const found = RUNTIME_ABIS.map((abi) => ({ abi, source: join(runtimeDir, abi, "libcraft.so") })).filter((entry) => existsSync(entry.source));
|
|
336
|
+
if (found.length === 0) {
|
|
337
|
+
throw new Error(`${runtimeDir} has no <abi>/libcraft.so for any of ${RUNTIME_ABIS.join(", ")}. ` + "Run `zig build build-android-all -Doptimize=ReleaseSafe` in packages/zig and point at its zig-out/android.");
|
|
338
|
+
}
|
|
339
|
+
if (found.length < RUNTIME_ABIS.length) {
|
|
340
|
+
const missing = RUNTIME_ABIS.filter((abi) => !found.some((entry) => entry.abi === abi));
|
|
341
|
+
console.warn(` \u26A0 only ${found.map((entry) => entry.abi).join(", ")} was found; ${missing.join(", ")} is missing. ` + "The app will fall back to the Kotlin shim on those devices.");
|
|
342
|
+
}
|
|
343
|
+
const dest = join(output, "app/src/main/jniLibs");
|
|
344
|
+
rmSync(dest, { force: true, recursive: true });
|
|
345
|
+
for (const { abi, source } of found) {
|
|
346
|
+
const abiDir = join(dest, abi);
|
|
347
|
+
mkdirSync(abiDir, { recursive: true });
|
|
348
|
+
cpSync(source, join(abiDir, "libcraft.so"));
|
|
349
|
+
}
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
96
352
|
async function init(options) {
|
|
97
353
|
const { name, packageName, output } = options;
|
|
98
354
|
console.log(`
|
|
99
355
|
\u26A1 Initializing Craft Android project: ${name}`);
|
|
100
356
|
console.log(` Output: ${output}
|
|
101
357
|
`);
|
|
102
|
-
const finalPackageName = packageName || `com.craft.${name
|
|
358
|
+
const finalPackageName = packageName || `com.craft.${generatedPackageSegment(name)}`;
|
|
103
359
|
const packagePath = finalPackageName.replace(/\./g, "/");
|
|
360
|
+
const config = {
|
|
361
|
+
...DEFAULT_CONFIG,
|
|
362
|
+
...options.config,
|
|
363
|
+
appName: name,
|
|
364
|
+
packageName: finalPackageName
|
|
365
|
+
};
|
|
366
|
+
if (config.enableBackgroundLocation)
|
|
367
|
+
config.enableGeolocation = true;
|
|
368
|
+
if (config.enableHealthConnect) {
|
|
369
|
+
config.minSdk = Math.max(config.minSdk ?? 26, 26);
|
|
370
|
+
config.compileSdk = Math.max(config.compileSdk ?? 36, 36);
|
|
371
|
+
}
|
|
372
|
+
normalizeAndroidNetworkConfig(config);
|
|
373
|
+
validateAndroidConfig(config);
|
|
374
|
+
if (config.enablePushNotifications && !config.googleServicesFile) {
|
|
375
|
+
throw new Error("Android push notifications require a googleServicesFile");
|
|
376
|
+
}
|
|
377
|
+
if (config.googleServicesFile)
|
|
378
|
+
validateGoogleServicesFile(config.googleServicesFile, finalPackageName);
|
|
379
|
+
if (config.appIconPath)
|
|
380
|
+
requireRegularFile(config.appIconPath, "App icon");
|
|
381
|
+
const appIconExtension = config.appIconPath ? extname(config.appIconPath).toLowerCase() : undefined;
|
|
382
|
+
if (config.appIconPath && ![".gif", ".jpg", ".png", ".webp"].includes(appIconExtension ?? "")) {
|
|
383
|
+
throw new Error(`Unsupported Android app icon format: ${appIconExtension || "(none)"}`);
|
|
384
|
+
}
|
|
104
385
|
const dirs = [
|
|
105
386
|
output,
|
|
106
387
|
join(output, "app/src/main/java", packagePath),
|
|
@@ -110,32 +391,24 @@ async function init(options) {
|
|
|
110
391
|
join(output, "app/src/main/assets"),
|
|
111
392
|
join(output, "gradle/wrapper")
|
|
112
393
|
];
|
|
394
|
+
if (existsSync(output) && !statSync(output).isDirectory()) {
|
|
395
|
+
throw new Error(`Android project output must be a directory: ${output}`);
|
|
396
|
+
}
|
|
113
397
|
for (const dir of dirs) {
|
|
114
398
|
if (!existsSync(dir)) {
|
|
115
399
|
mkdirSync(dir, { recursive: true });
|
|
116
400
|
}
|
|
117
401
|
}
|
|
118
|
-
|
|
119
|
-
...DEFAULT_CONFIG,
|
|
120
|
-
appName: name,
|
|
121
|
-
packageName: finalPackageName,
|
|
122
|
-
...options.config
|
|
123
|
-
};
|
|
124
|
-
if (config.enableBackgroundLocation)
|
|
125
|
-
config.enableGeolocation = true;
|
|
126
|
-
writeFileSync(join(output, "craft.config.json"), JSON.stringify(config, null, 2));
|
|
127
|
-
writeFileSync(join(output, "app/src/main/assets/craft.config.json"), JSON.stringify(config, null, 2));
|
|
402
|
+
writeAndroidConfig(output, config);
|
|
128
403
|
const hasGoogleServices = Boolean(config.googleServicesFile);
|
|
129
404
|
if (config.googleServicesFile) {
|
|
130
|
-
if (!existsSync(config.googleServicesFile))
|
|
131
|
-
throw new Error(`Google services file not found: ${config.googleServicesFile}`);
|
|
132
405
|
cpSync(config.googleServicesFile, join(output, "app/google-services.json"));
|
|
133
406
|
}
|
|
134
407
|
const mainActivityTemplate = readFileSync(join(TEMPLATES_DIR, "MainActivity.kt.template"), "utf-8");
|
|
135
408
|
const mainActivity = mainActivityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name);
|
|
136
409
|
writeFileSync(join(output, "app/src/main/java", packagePath, "MainActivity.kt"), mainActivity);
|
|
137
410
|
const craftBridgeTemplate = readFileSync(join(TEMPLATES_DIR, "CraftBridge.kt.template"), "utf-8");
|
|
138
|
-
const craftBridge = craftBridgeTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{ENABLE_SPEECH\}\}/g, String(Boolean(config.enableSpeechRecognition))).replace(/\{\{ENABLE_HAPTICS\}\}/g, String(Boolean(config.enableHaptics))).replace(/\{\{ENABLE_SHARE\}\}/g, String(Boolean(config.enableShare))).replace(/\{\{ENABLE_CAMERA\}\}/g, String(Boolean(config.enableCamera))).replace(/\{\{ENABLE_BIOMETRIC\}\}/g, String(Boolean(config.enableBiometric))).replace(/\{\{ENABLE_PUSH\}\}/g, String(Boolean(config.enablePushNotifications))).replace(/\{\{ENABLE_SECURE_STORAGE\}\}/g, String(Boolean(config.enableSecureStorage))).replace(/\{\{ENABLE_GEOLOCATION\}\}/g, String(Boolean(config.enableGeolocation))).replace(/\{\{ENABLE_BACKGROUND_LOCATION\}\}/g, String(Boolean(config.enableBackgroundLocation))).replace(/\{\{ENABLE_KEEP_AWAKE\}\}/g, String(Boolean(config.enableKeepAwake))).replace(/\{\{ENABLE_DEEP_LINKS\}\}/g, String(Boolean(config.enableDeepLinks))).replace(/\{\{ENABLE_HEALTH_CONNECT\}\}/g, String(Boolean(config.enableHealthConnect))).replace(/\{\{FIREBASE_IMPORT\}\}/g, config.enablePushNotifications ? "import com.google.firebase.messaging.FirebaseMessaging" : "").replace(/\{\{REGISTER_PUSH_IMPLEMENTATION\}\}/g, config.enablePushNotifications ? `activity.runOnUiThread {
|
|
411
|
+
const craftBridge = craftBridgeTemplate.replace(/\{\{PROMISE_RUNTIME\}\}/g, () => renderAndroidPromiseRuntime(" ")).replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{ENABLE_SPEECH\}\}/g, String(Boolean(config.enableSpeechRecognition))).replace(/\{\{ENABLE_HAPTICS\}\}/g, String(Boolean(config.enableHaptics))).replace(/\{\{ENABLE_SHARE\}\}/g, String(Boolean(config.enableShare))).replace(/\{\{ENABLE_CAMERA\}\}/g, String(Boolean(config.enableCamera))).replace(/\{\{ENABLE_BIOMETRIC\}\}/g, String(Boolean(config.enableBiometric))).replace(/\{\{ENABLE_PUSH\}\}/g, String(Boolean(config.enablePushNotifications))).replace(/\{\{ENABLE_SECURE_STORAGE\}\}/g, String(Boolean(config.enableSecureStorage))).replace(/\{\{ENABLE_GEOLOCATION\}\}/g, String(Boolean(config.enableGeolocation))).replace(/\{\{ENABLE_BACKGROUND_LOCATION\}\}/g, String(Boolean(config.enableBackgroundLocation))).replace(/\{\{ENABLE_KEEP_AWAKE\}\}/g, String(Boolean(config.enableKeepAwake))).replace(/\{\{ENABLE_DEEP_LINKS\}\}/g, String(Boolean(config.enableDeepLinks))).replace(/\{\{ENABLE_HEALTH_CONNECT\}\}/g, String(Boolean(config.enableHealthConnect))).replace(/\{\{FIREBASE_IMPORT\}\}/g, config.enablePushNotifications ? "import com.google.firebase.messaging.FirebaseMessaging" : "").replace(/\{\{REGISTER_PUSH_IMPLEMENTATION\}\}/g, config.enablePushNotifications ? `activity.runOnUiThread {
|
|
139
412
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
|
140
413
|
&& ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
|
141
414
|
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 4204)
|
|
@@ -145,27 +418,26 @@ async function init(options) {
|
|
|
145
418
|
val token = if (task.isSuccessful) task.result else null
|
|
146
419
|
val callback = if (token.isNullOrBlank()) "window._craftPushReject" else "window._craftPushResolve"
|
|
147
420
|
val payload = JSONObject.quote(token ?: task.exception?.message ?: "Firebase Cloud Messaging is not configured")
|
|
148
|
-
|
|
421
|
+
evaluatePromiseJavascript("$callback && $callback($payload)")
|
|
149
422
|
}
|
|
150
423
|
} catch (error: Exception) {
|
|
151
424
|
val payload = JSONObject.quote(error.message ?: "Firebase Cloud Messaging is not configured")
|
|
152
|
-
|
|
425
|
+
evaluatePromiseJavascript("window._craftPushReject && window._craftPushReject($payload)")
|
|
153
426
|
}
|
|
154
|
-
}` : `
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
null
|
|
158
|
-
)
|
|
159
|
-
}`);
|
|
427
|
+
}` : `evaluatePromiseJavascript(
|
|
428
|
+
"window._craftPushReject && window._craftPushReject('Push notifications are disabled')"
|
|
429
|
+
)`);
|
|
160
430
|
writeFileSync(join(output, "app/src/main/java", packagePath, "CraftBridge.kt"), craftBridge);
|
|
431
|
+
const craftNative = readFileSync(join(TEMPLATES_DIR, "CraftNative.kt.template"), "utf-8");
|
|
432
|
+
const nativeDir = join(output, "app/src/main/java/com/craft/runtime");
|
|
433
|
+
mkdirSync(nativeDir, { recursive: true });
|
|
434
|
+
writeFileSync(join(nativeDir, "CraftNative.kt"), craftNative);
|
|
435
|
+
const serviceTemplate = readFileSync(join(TEMPLATES_DIR, "LocationRecordingService.kt.template"), "utf-8");
|
|
436
|
+
writeFileSync(join(nativeDir, "LocationRecordingService.kt"), serviceTemplate);
|
|
161
437
|
const healthTemplate = readFileSync(join(TEMPLATES_DIR, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
|
|
162
438
|
writeFileSync(join(output, "app/src/main/java", packagePath, "CraftHealthConnect.kt"), healthTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
163
|
-
if (config.enableBackgroundLocation) {
|
|
164
|
-
const serviceTemplate = readFileSync(join(TEMPLATES_DIR, "LocationRecordingService.kt.template"), "utf-8");
|
|
165
|
-
writeFileSync(join(output, "app/src/main/java", packagePath, "LocationRecordingService.kt"), serviceTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
166
|
-
}
|
|
167
439
|
const manifestTemplate = readFileSync(join(TEMPLATES_DIR, "AndroidManifest.xml.template"), "utf-8");
|
|
168
|
-
const manifest = manifestTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{PERMISSIONS\}\}/g, renderAndroidPermissions(config)).replace(/\{\{USES_CLEARTEXT\}\}/g, config.devServerURL?.startsWith("http://") ? "true" : "false").replace(/\{\{DEEP_LINK_INTENT_FILTERS\}\}/g, renderAndroidDeepLinks(config)).replace(/\{\{BACKGROUND_SERVICE\}\}/g, config.enableBackgroundLocation ? ' <service android:name=".LocationRecordingService" android:exported="false" android:foregroundServiceType="location" android:stopWithTask="false" />' : "").replace(/\{\{HEALTH_CONNECT_QUERIES\}\}/g, config.enableHealthConnect ? ` <queries>
|
|
440
|
+
const manifest = manifestTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{PERMISSIONS\}\}/g, renderAndroidPermissions(config)).replace(/\{\{USES_CLEARTEXT\}\}/g, config.devServerURL?.startsWith("http://") ? "true" : "false").replace(/\{\{DEEP_LINK_INTENT_FILTERS\}\}/g, renderAndroidDeepLinks(config)).replace(/\{\{BACKGROUND_SERVICE\}\}/g, config.enableBackgroundLocation ? ' <service android:name="com.craft.runtime.LocationRecordingService" android:exported="false" android:foregroundServiceType="location" android:stopWithTask="false" />' : "").replace(/\{\{HEALTH_CONNECT_QUERIES\}\}/g, config.enableHealthConnect ? ` <queries>
|
|
169
441
|
<package android:name="com.google.android.apps.healthdata" />
|
|
170
442
|
</queries>` : "").replace(/\{\{HEALTH_CONNECT_RATIONALE\}\}/g, config.enableHealthConnect ? ` <intent-filter>
|
|
171
443
|
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
|
|
@@ -178,13 +450,13 @@ async function init(options) {
|
|
|
178
450
|
const projectGradleTemplate = readFileSync(join(TEMPLATES_DIR, "build.gradle.kts.project.template"), "utf-8");
|
|
179
451
|
writeFileSync(join(output, "build.gradle.kts"), projectGradleTemplate.replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services") version "4.4.2" apply false' : ""));
|
|
180
452
|
const appGradleTemplate = readFileSync(join(TEMPLATES_DIR, "build.gradle.kts.app.template"), "utf-8");
|
|
181
|
-
const appGradle = appGradleTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{VERSION_NAME\}\}/g, config.version || "1.0.0").replace(/\{\{VERSION_CODE\}\}/g, String(config.versionCode || 1)).replace(/\{\{MIN_SDK\}\}/g, String(config.minSdk || 24)).replace(/\{\{COMPILE_SDK\}\}/g, String(
|
|
453
|
+
const appGradle = appGradleTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{VERSION_NAME\}\}/g, escapeKotlinString(config.version || "1.0.0")).replace(/\{\{VERSION_CODE\}\}/g, String(config.versionCode || 1)).replace(/\{\{MIN_SDK\}\}/g, String(config.minSdk || 24)).replace(/\{\{COMPILE_SDK\}\}/g, String(config.compileSdk || 36)).replace(/\{\{TARGET_SDK\}\}/g, String(config.targetSdk || 35)).replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services")' : "").replace(/\{\{FIREBASE_MESSAGING_DEPENDENCY\}\}/g, config.enablePushNotifications ? ' implementation("com.google.firebase:firebase-messaging:24.1.0")' : "").replace(/\{\{HEALTH_CONNECT_DEPENDENCIES\}\}/g, config.enableHealthConnect ? ` implementation("androidx.health.connect:connect-client:1.1.0")
|
|
182
454
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")` : "");
|
|
183
455
|
writeFileSync(join(output, "app/build.gradle.kts"), appGradle);
|
|
184
456
|
const proguardTemplate = readFileSync(join(TEMPLATES_DIR, "proguard-rules.pro.template"), "utf-8");
|
|
185
457
|
writeFileSync(join(output, "app/proguard-rules.pro"), proguardTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
186
458
|
const settingsTemplate = readFileSync(join(TEMPLATES_DIR, "settings.gradle.kts.template"), "utf-8");
|
|
187
|
-
const settings = settingsTemplate.replace(/\{\{APP_NAME\}\}/g, name);
|
|
459
|
+
const settings = settingsTemplate.replace(/\{\{APP_NAME\}\}/g, escapeKotlinString(generatedGradleProjectName(name)));
|
|
188
460
|
writeFileSync(join(output, "settings.gradle.kts"), settings);
|
|
189
461
|
const gradleProperties = `org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|
190
462
|
android.useAndroidX=true
|
|
@@ -205,7 +477,7 @@ zipStorePath=wrapper/dists
|
|
|
205
477
|
writeFileSync(join(output, "gradle/wrapper/gradle-wrapper.properties"), gradleWrapperProps);
|
|
206
478
|
const stringsXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
207
479
|
<resources>
|
|
208
|
-
<string name="app_name">${name}</string>
|
|
480
|
+
<string name="app_name">${escapeXml(name)}</string>
|
|
209
481
|
</resources>
|
|
210
482
|
`;
|
|
211
483
|
writeFileSync(join(output, "app/src/main/res/values/strings.xml"), stringsXml);
|
|
@@ -229,9 +501,7 @@ zipStorePath=wrapper/dists
|
|
|
229
501
|
</vector>
|
|
230
502
|
`;
|
|
231
503
|
if (config.appIconPath) {
|
|
232
|
-
|
|
233
|
-
throw new Error(`App icon not found: ${config.appIconPath}`);
|
|
234
|
-
cpSync(config.appIconPath, join(output, "app/src/main/res/drawable/craft_app_icon.png"));
|
|
504
|
+
cpSync(config.appIconPath, join(output, `app/src/main/res/drawable/craft_app_icon${appIconExtension}`));
|
|
235
505
|
} else {
|
|
236
506
|
writeFileSync(join(output, "app/src/main/res/drawable/craft_app_icon.xml"), appIconXml);
|
|
237
507
|
}
|
|
@@ -265,7 +535,7 @@ zipStorePath=wrapper/dists
|
|
|
265
535
|
<head>
|
|
266
536
|
<meta charset="UTF-8">
|
|
267
537
|
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
268
|
-
<title>${name}</title>
|
|
538
|
+
<title>${escapeXml(name)}</title>
|
|
269
539
|
<style>
|
|
270
540
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
271
541
|
body {
|
|
@@ -285,7 +555,7 @@ zipStorePath=wrapper/dists
|
|
|
285
555
|
</head>
|
|
286
556
|
<body>
|
|
287
557
|
<div class="container">
|
|
288
|
-
<h1>\u26A1 ${name}</h1>
|
|
558
|
+
<h1>\u26A1 ${escapeXml(name)}</h1>
|
|
289
559
|
<p>Built with Craft Android</p>
|
|
290
560
|
<p class="ready" id="status">Waiting for Craft bridge...</p>
|
|
291
561
|
</div>
|
|
@@ -298,6 +568,11 @@ zipStorePath=wrapper/dists
|
|
|
298
568
|
</body>
|
|
299
569
|
</html>`;
|
|
300
570
|
writeFileSync(join(output, "app/src/main/assets/index.html"), placeholderHtml);
|
|
571
|
+
const runtimeDir = resolveRuntimeDir(options.runtimeDir);
|
|
572
|
+
if (runtimeDir) {
|
|
573
|
+
installRuntime(output, runtimeDir);
|
|
574
|
+
console.log(" Installed the Zig runtime from", runtimeDir);
|
|
575
|
+
}
|
|
301
576
|
console.log("\u2705 Project initialized");
|
|
302
577
|
console.log("");
|
|
303
578
|
console.log("Next steps:");
|
|
@@ -316,18 +591,29 @@ async function build(options) {
|
|
|
316
591
|
throw new Error(`No craft.config.json found in ${output}. Run 'craft android init' first.`);
|
|
317
592
|
}
|
|
318
593
|
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
594
|
+
if (existsSync(join(output, "app/src/main/jniLibs"))) {
|
|
595
|
+
const runtimeDir = resolveRuntimeDir(options.runtimeDir);
|
|
596
|
+
if (runtimeDir) {
|
|
597
|
+
installRuntime(output, runtimeDir);
|
|
598
|
+
console.log(" Refreshed the Zig runtime from", runtimeDir);
|
|
599
|
+
} else {
|
|
600
|
+
console.log(" Keeping the Zig runtime installed at init (no runtime directory configured)");
|
|
601
|
+
}
|
|
602
|
+
}
|
|
319
603
|
if (devServer) {
|
|
320
|
-
|
|
321
|
-
config.
|
|
322
|
-
|
|
323
|
-
|
|
604
|
+
const url = androidWebUrl(devServer, "Android dev server URL");
|
|
605
|
+
config.devServerURL = url.toString();
|
|
606
|
+
config.trustedOrigins = [...new Set([
|
|
607
|
+
...(config.trustedOrigins ?? []).map((value) => androidWebUrl(value, "Android trusted origin").origin),
|
|
608
|
+
url.origin
|
|
609
|
+
])];
|
|
610
|
+
writeAndroidConfig(output, config);
|
|
324
611
|
console.log(` Dev server: ${devServer}`);
|
|
325
612
|
}
|
|
326
613
|
if (htmlPath) {
|
|
327
614
|
syncAndroidWebAssets(htmlPath, output);
|
|
328
615
|
config.hasBundledFallback = Boolean(devServer);
|
|
329
|
-
|
|
330
|
-
writeFileSync(join(output, "app/src/main/assets/craft.config.json"), JSON.stringify(config, null, 2));
|
|
616
|
+
writeAndroidConfig(output, config);
|
|
331
617
|
console.log(` Synced: ${htmlPath} \u2192 assets/`);
|
|
332
618
|
}
|
|
333
619
|
if (!compile)
|
|
@@ -400,9 +686,11 @@ async function run(options) {
|
|
|
400
686
|
export {
|
|
401
687
|
syncAndroidWebAssets,
|
|
402
688
|
run,
|
|
689
|
+
resolveRuntimeDir,
|
|
403
690
|
renderAndroidPermissions,
|
|
404
691
|
renderAndroidDeepLinks,
|
|
405
692
|
open,
|
|
693
|
+
installRuntime,
|
|
406
694
|
init,
|
|
407
695
|
build
|
|
408
696
|
};
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
declare const ANDROID_PROMISE_RUNTIME = "\nif (window.__craftRejectPendingPromises) {\n window.__craftRejectPendingPromises('Android bridge reinitialized');\n}\nif (window.__craftRejectPermissionRequests) {\n window.__craftRejectPermissionRequests('Android bridge reinitialized');\n}\nwindow.__craftPromiseRuntimeClosed = false;\nwindow.__craftPendingPromises = Object.create(null);\nwindow.__craftPromise = function(channel, resolveName, rejectName, invoke, timeoutMs, timeoutError) {\n if (window.__craftPromiseRuntimeClosed) {\n return Promise.reject(new Error('Android bridge is closed'));\n }\n if (window.__craftPendingPromises[channel]) {\n return Promise.reject(new Error('A '.concat(channel, ' request is already in progress')));\n }\n\n return new Promise(function(resolve, reject) {\n var entry = {settled: false, timer: null, settle: null};\n var resolveCallback;\n var rejectCallback;\n\n entry.settle = function(succeeded, value) {\n if (entry.settled || window.__craftPendingPromises[channel] !== entry) return;\n entry.settled = true;\n if (entry.timer !== null) clearTimeout(entry.timer);\n if (window[resolveName] === resolveCallback) window[resolveName] = null;\n if (window[rejectName] === rejectCallback) window[rejectName] = null;\n delete window.__craftPendingPromises[channel];\n if (succeeded) resolve(value);\n else reject(value);\n };\n\n resolveCallback = function(value) {\n entry.settle(true, value);\n };\n rejectCallback = function(error) {\n entry.settle(false, error);\n };\n window[resolveName] = resolveCallback;\n window[rejectName] = rejectCallback;\n window.__craftPendingPromises[channel] = entry;\n\n if (timeoutMs > 0) {\n entry.timer = setTimeout(function() {\n entry.settle(false, timeoutError || new Error(channel.concat(' request timed out')));\n }, timeoutMs);\n }\n\n try {\n invoke();\n }\n catch (error) {\n entry.settle(false, error);\n }\n });\n};\n\nwindow.__craftRejectPendingPromises = function(message) {\n window.__craftPromiseRuntimeClosed = true;\n Object.keys(window.__craftPendingPromises).forEach(function(channel) {\n var entry = window.__craftPendingPromises[channel];\n if (entry) entry.settle(false, new Error(message || 'Android bridge closed'));\n });\n};\n";
|
|
2
|
+
export declare function renderAndroidPromiseRuntime(indent?: string): string;
|
|
3
|
+
export { ANDROID_PROMISE_RUNTIME };
|