craft-native 0.0.236 → 0.0.238
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 +71 -0
- package/dist/android/src/emulator.d.ts +23 -0
- package/dist/android/src/index.d.ts +7 -1
- package/dist/android/src/index.js +280 -98
- package/dist/android/templates/CraftBridge.kt.template +65 -54
- package/dist/android/templates/CraftNative.kt.template +27 -0
- package/dist/android/templates/CraftNativeCapabilities.kt.template +55 -6
- package/dist/android/templates/CraftNativeServices.kt.template +45 -35
- package/dist/android/templates/CraftNativeShell.kt.template +103 -20
- package/dist/android/templates/LocationRecordingService.kt.template +252 -46
- package/dist/android/templates/MainActivityNative.kt.template +163 -5
- package/dist/android-adb.d.ts +2 -0
- package/dist/api/index.d.ts +1 -1
- package/dist/api/mobile.d.ts +49 -14
- package/dist/browser/storage.d.ts +3 -0
- package/dist/browser.js +163 -51
- package/dist/cli.js +1 -1
- package/dist/components/index.d.ts +33 -0
- package/dist/index.cjs +126 -12
- package/dist/index.js +126 -12
- package/dist/ios/src/app-shortcuts.d.ts +11 -0
- package/dist/ios/src/index.d.ts +7 -0
- package/dist/ios/src/index.js +65 -1
- package/dist/ios/src/testing.d.ts +2 -0
- package/dist/ios/templates/CraftApp.swift +392 -88
- package/dist/ios/templates/CraftHeartRate.swift +141 -44
- package/dist/ios/templates/CraftNativeActions.swift +46 -48
- package/dist/ios/templates/CraftNativeScreen.swift +177 -10
- package/dist/ios/templates/CraftPersistentStorage.swift +99 -0
- package/dist/ios/templates/CraftRecordingArchive.swift +85 -0
- package/dist/ios/templates/testing/DriverTests.swift.template +8 -1
- package/dist/mobile-testing.js +30 -4
- package/dist/mobile.js +169 -17
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../android/src/index.ts
|
|
3
|
-
import { cpSync, existsSync, mkdirSync, readFileSync as readFileSync2, rmSync, statSync, writeFileSync } from "fs";
|
|
4
|
-
import { dirname as dirname2, extname, isAbsolute, join as
|
|
3
|
+
import { cpSync, existsSync, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync, writeFileSync } from "fs";
|
|
4
|
+
import { dirname as dirname2, extname, isAbsolute as isAbsolute2, join as join3, relative, resolve as resolve2, sep } from "path";
|
|
5
5
|
var {$ } = globalThis.Bun;
|
|
6
6
|
|
|
7
7
|
// ../android/src/promise-runtime.ts
|
|
@@ -132,10 +132,191 @@ tasks.register('runFixture', JavaExec) {
|
|
|
132
132
|
await rm(directory, { recursive: true, force: true });
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
|
+
// ../android/src/emulator.ts
|
|
136
|
+
import { appendFileSync, closeSync, mkdirSync, mkdtempSync, openSync, rmSync } from "fs";
|
|
137
|
+
import { availableParallelism, cpus, tmpdir as tmpdir2, totalmem } from "os";
|
|
138
|
+
import { basename, isAbsolute, join as join2, resolve } from "path";
|
|
139
|
+
async function createAndroidEmulator(options) {
|
|
140
|
+
if (!["darwin", "linux"].includes(process.platform))
|
|
141
|
+
throw new Error("Android emulator sessions require macOS or Linux");
|
|
142
|
+
if (!["hardware", "software"].includes(options.acceleration))
|
|
143
|
+
throw new Error("Select hardware or software Android emulation explicitly");
|
|
144
|
+
if (![options.sdkmanager, options.avdmanager].every(isAbsolute))
|
|
145
|
+
throw new Error("Select absolute Android command-line tool paths");
|
|
146
|
+
const abi = process.arch === "arm64" ? "arm64-v8a" : process.arch === "x64" ? "x86_64" : undefined;
|
|
147
|
+
if (!abi || !/^system-images;android-\d+;[^;\s]+;[^;\s]+$/.test(options.image) || !options.image.endsWith(`;${abi}`))
|
|
148
|
+
throw new Error("The Android system image must match the host architecture");
|
|
149
|
+
if (options.acceleration === "software" && process.arch !== "x64")
|
|
150
|
+
throw new Error("Android software emulation requires an x86_64 host and system image");
|
|
151
|
+
const bootTimeoutMs = options.bootTimeoutMs ?? 600000;
|
|
152
|
+
const maximumBootTimeoutMs = options.acceleration === "software" ? 1200000 : 600000;
|
|
153
|
+
if (!Number.isSafeInteger(bootTimeoutMs) || bootTimeoutMs < 1000 || bootTimeoutMs > maximumBootTimeoutMs)
|
|
154
|
+
throw new Error(`Android bootTimeoutMs must be an integer between 1000 and ${maximumBootTimeoutMs} milliseconds`);
|
|
155
|
+
const port = options.port ?? 5558;
|
|
156
|
+
if (!Number.isInteger(port) || port < 5554 || port > 5682 || port % 2 !== 0)
|
|
157
|
+
throw new Error("Android console ports must be even and between 5554 and 5682");
|
|
158
|
+
const sdk = resolve(options.sdkRoot);
|
|
159
|
+
const artifacts = resolve(options.artifactsDirectory);
|
|
160
|
+
const temporary = options.temporaryDirectory ?? tmpdir2();
|
|
161
|
+
mkdirSync(artifacts, { recursive: true });
|
|
162
|
+
mkdirSync(temporary, { recursive: true });
|
|
163
|
+
const owned = mkdtempSync(join2(temporary, "craft-android-emulator-"));
|
|
164
|
+
const name = basename(owned);
|
|
165
|
+
const serial = `emulator-${port}`;
|
|
166
|
+
const env = {
|
|
167
|
+
...process.env,
|
|
168
|
+
...options.env,
|
|
169
|
+
ANDROID_HOME: sdk,
|
|
170
|
+
ANDROID_SDK_ROOT: sdk,
|
|
171
|
+
ANDROID_SDK_HOME: owned,
|
|
172
|
+
ANDROID_USER_HOME: join2(owned, ".android"),
|
|
173
|
+
ANDROID_EMULATOR_HOME: join2(owned, ".android"),
|
|
174
|
+
ANDROID_AVD_HOME: join2(owned, "avd"),
|
|
175
|
+
ANDROID_SERIAL: serial,
|
|
176
|
+
AVDMANAGER_OPTS: `'-Dcom.android.sdkmanager.toolsdir=${join2(sdk, "cmdline-tools/latest")}'`
|
|
177
|
+
};
|
|
178
|
+
const adb = join2(sdk, "platform-tools/adb");
|
|
179
|
+
const executable = join2(sdk, "emulator/emulator");
|
|
180
|
+
let child;
|
|
181
|
+
let log;
|
|
182
|
+
let created = false;
|
|
183
|
+
let sequence = 0;
|
|
184
|
+
let disposal;
|
|
185
|
+
let bootStartedAt = 0;
|
|
186
|
+
let cpuStart = 0;
|
|
187
|
+
let idleStart = 0;
|
|
188
|
+
let identity;
|
|
189
|
+
async function recordBootResult(status, endedAt = Date.now()) {
|
|
190
|
+
if (!identity)
|
|
191
|
+
return;
|
|
192
|
+
const cpuEnd = cpus();
|
|
193
|
+
const totalDeltaMs = cpuEnd.reduce((total, cpu) => total + Object.values(cpu.times).reduce((sum, value) => sum + value, 0), 0) - cpuStart;
|
|
194
|
+
const idleDeltaMs = cpuEnd.reduce((total, cpu) => total + cpu.times.idle, 0) - idleStart;
|
|
195
|
+
await Bun.write(join2(artifacts, "android-emulator.json"), JSON.stringify({
|
|
196
|
+
...identity,
|
|
197
|
+
status,
|
|
198
|
+
[status === "booted" ? "bootedAt" : "failedAt"]: new Date(endedAt).toISOString(),
|
|
199
|
+
bootElapsedMs: endedAt - bootStartedAt,
|
|
200
|
+
hostCpuInterval: { totalDeltaMs, idleDeltaMs, busyDeltaMs: totalDeltaMs - idleDeltaMs }
|
|
201
|
+
}, null, 2) + `
|
|
202
|
+
`);
|
|
203
|
+
}
|
|
204
|
+
async function run(args, input, timeout = 900000, allowFailure = false) {
|
|
205
|
+
const startedAt = new Date().toISOString();
|
|
206
|
+
const command = Bun.spawn(args, { env, stdin: input === undefined ? "ignore" : new TextEncoder().encode(input), stdout: "pipe", stderr: "pipe", signal: AbortSignal.timeout(timeout) });
|
|
207
|
+
const [code, stdout, stderr] = await Promise.all([command.exited, new Response(command.stdout).text(), new Response(command.stderr).text()]);
|
|
208
|
+
appendFileSync(join2(artifacts, "device-commands.jsonl"), JSON.stringify({ sequence: ++sequence, startedAt, finishedAt: new Date().toISOString(), args, code, stdout, stderr }) + `
|
|
209
|
+
`);
|
|
210
|
+
if (code !== 0 && !allowFailure)
|
|
211
|
+
throw new Error(`${basename(args[0])} failed with exit ${code}; inspect ${artifacts}`);
|
|
212
|
+
return { code, stdout };
|
|
213
|
+
}
|
|
214
|
+
const dispose = () => disposal ??= (async () => {
|
|
215
|
+
try {
|
|
216
|
+
if (child && child.exitCode === null) {
|
|
217
|
+
child.kill("SIGTERM");
|
|
218
|
+
await Promise.race([child.exited, Bun.sleep(1e4)]);
|
|
219
|
+
if (child.exitCode === null)
|
|
220
|
+
child.kill("SIGKILL");
|
|
221
|
+
await child.exited;
|
|
222
|
+
}
|
|
223
|
+
if (created)
|
|
224
|
+
await run([options.avdmanager, "delete", "avd", "--name", name], undefined, 30000);
|
|
225
|
+
} finally {
|
|
226
|
+
if (log !== undefined)
|
|
227
|
+
closeSync(log);
|
|
228
|
+
rmSync(owned, { recursive: true, force: true });
|
|
229
|
+
}
|
|
230
|
+
})();
|
|
231
|
+
try {
|
|
232
|
+
mkdirSync(env.ANDROID_AVD_HOME, { recursive: true });
|
|
233
|
+
mkdirSync(env.ANDROID_USER_HOME, { recursive: true });
|
|
234
|
+
if (options.acceptLicenses)
|
|
235
|
+
await run([options.sdkmanager, `--sdk_root=${sdk}`, "--licenses"], `y
|
|
236
|
+
`.repeat(100));
|
|
237
|
+
await run([options.sdkmanager, `--sdk_root=${sdk}`, "platform-tools", "emulator"], options.acceptLicenses ? `y
|
|
238
|
+
`.repeat(100) : undefined);
|
|
239
|
+
const acceleration = await run([executable, "-accel-check"], undefined, 30000, true);
|
|
240
|
+
if (options.acceleration === "hardware" && acceleration.code !== 0)
|
|
241
|
+
throw new Error(`Android hardware acceleration is unavailable; inspect ${artifacts}`);
|
|
242
|
+
await run([options.sdkmanager, `--sdk_root=${sdk}`, options.image], options.acceptLicenses ? `y
|
|
243
|
+
`.repeat(100) : undefined);
|
|
244
|
+
const devices = (await run([adb, "devices"], undefined, 1e4)).stdout;
|
|
245
|
+
if (devices.split(`
|
|
246
|
+
`).some((line) => line.startsWith(`${serial} `)))
|
|
247
|
+
throw new Error(`${serial} is already in use`);
|
|
248
|
+
await run([options.avdmanager, "create", "avd", "--name", name, "--package", options.image, "--device", "pixel_6"], `no
|
|
249
|
+
`);
|
|
250
|
+
created = true;
|
|
251
|
+
log = openSync(join2(artifacts, "emulator.log"), "w");
|
|
252
|
+
bootStartedAt = Date.now();
|
|
253
|
+
const hostCpus = cpus();
|
|
254
|
+
cpuStart = hostCpus.reduce((total, cpu) => total + Object.values(cpu.times).reduce((sum, value) => sum + value, 0), 0);
|
|
255
|
+
idleStart = hostCpus.reduce((total, cpu) => total + cpu.times.idle, 0);
|
|
256
|
+
child = Bun.spawn([executable, "-avd", name, "-port", String(port), "-no-window", "-no-snapshot", "-no-audio", "-no-boot-anim", "-gpu", "software", "-memory", "2048", "-cores", "2", "-accel", options.acceleration === "software" ? "off" : "on"], { env, stdout: log, stderr: log });
|
|
257
|
+
identity = {
|
|
258
|
+
image: options.image,
|
|
259
|
+
acceleration: options.acceleration,
|
|
260
|
+
accelerationProbeExitCode: acceleration.code,
|
|
261
|
+
bootTimeoutMs,
|
|
262
|
+
bootStartedAt: new Date(bootStartedAt).toISOString(),
|
|
263
|
+
host: { cpuModel: hostCpus[0]?.model, logicalCpus: hostCpus.length, availableParallelism: availableParallelism(), memoryBytes: totalmem() },
|
|
264
|
+
virtualCpus: 2,
|
|
265
|
+
memoryMb: 2048,
|
|
266
|
+
serial,
|
|
267
|
+
avdDirectory: env.ANDROID_AVD_HOME,
|
|
268
|
+
emulatorPid: child.pid
|
|
269
|
+
};
|
|
270
|
+
await Bun.write(join2(artifacts, "android-emulator.json"), JSON.stringify(identity, null, 2) + `
|
|
271
|
+
`);
|
|
272
|
+
const deadline = bootStartedAt + bootTimeoutMs;
|
|
273
|
+
const probeTimeout = () => Math.max(1, Math.min(1e4, deadline - Date.now()));
|
|
274
|
+
while (Date.now() < deadline && child.exitCode === null) {
|
|
275
|
+
let booted = false;
|
|
276
|
+
const connection = await run([adb, "devices"], undefined, probeTimeout(), true);
|
|
277
|
+
if (connection.code === 0 && connection.stdout.includes(`${serial} device`)) {
|
|
278
|
+
const completion = await run([adb, "-s", serial, "shell", "getprop", "sys.boot_completed"], undefined, probeTimeout(), true);
|
|
279
|
+
if (completion.code === 0 && completion.stdout.trim() === "1") {
|
|
280
|
+
const settings = await run([adb, "-s", serial, "shell", "settings", "get", "global", "animator_duration_scale"], undefined, probeTimeout(), true);
|
|
281
|
+
const packages = await run([adb, "-s", serial, "shell", "pm", "path", "android"], undefined, probeTimeout(), true);
|
|
282
|
+
const systemUi = await run([adb, "-s", serial, "shell", "pidof", "com.android.systemui"], undefined, probeTimeout(), true);
|
|
283
|
+
const events = await run([adb, "-s", serial, "logcat", "-b", "events", "-d", "-t", "1"], undefined, probeTimeout(), true);
|
|
284
|
+
booted = settings.code === 0 && packages.code === 0 && systemUi.code === 0 && events.code === 0 && /^package:\/\S+/m.test(packages.stdout) && /^\d+(?:\s+\d+)*$/.test(systemUi.stdout.trim());
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const checkedAt = Date.now();
|
|
288
|
+
if (booted && checkedAt <= deadline) {
|
|
289
|
+
await recordBootResult("booted", checkedAt);
|
|
290
|
+
return { serial, adb, env, dispose };
|
|
291
|
+
}
|
|
292
|
+
await Bun.sleep(Math.min(2000, Math.max(0, deadline - Date.now())));
|
|
293
|
+
}
|
|
294
|
+
throw new Error(`The owned Android emulator did not boot within ${bootTimeoutMs}ms; inspect ${artifacts}`);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (child) {
|
|
297
|
+
try {
|
|
298
|
+
await recordBootResult("failed");
|
|
299
|
+
} catch (diagnostic) {
|
|
300
|
+
console.error(`Could not retain emulator boot timing: ${diagnostic}`);
|
|
301
|
+
}
|
|
302
|
+
for (const args of [["logcat", "-d", "-b", "all"], ["shell", "getprop"], ["shell", "dumpsys", "activity", "processes"]]) {
|
|
303
|
+
try {
|
|
304
|
+
await run([adb, "-s", serial, ...args], undefined, 1e4, true);
|
|
305
|
+
} catch {}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
try {
|
|
309
|
+
await dispose();
|
|
310
|
+
} catch (cleanup) {
|
|
311
|
+
throw new AggregateError([error, cleanup], "Android emulator setup and cleanup failed");
|
|
312
|
+
}
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
135
316
|
|
|
136
317
|
// ../android/src/index.ts
|
|
137
318
|
function syncNativeFonts(fonts, output) {
|
|
138
|
-
const destination =
|
|
319
|
+
const destination = join3(output, "app/src/main/assets/NativeFonts");
|
|
139
320
|
const names = new Set;
|
|
140
321
|
const files = Object.entries(fonts ?? {}).map(([family, entry]) => {
|
|
141
322
|
if (!/^[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(family))
|
|
@@ -143,7 +324,7 @@ function syncNativeFonts(fonts, output) {
|
|
|
143
324
|
if (names.has(family.toLowerCase()))
|
|
144
325
|
throw new Error(`Duplicate native font name: ${family}`);
|
|
145
326
|
names.add(family.toLowerCase());
|
|
146
|
-
if ([entry.src, entry.license].some((file) => file &&
|
|
327
|
+
if ([entry.src, entry.license].some((file) => file && resolve2(file).startsWith(resolve2(destination) + sep)))
|
|
147
328
|
throw new Error("Native font sources must be outside the generated NativeFonts directory");
|
|
148
329
|
const extension = extname(entry.src).toLowerCase();
|
|
149
330
|
if (![".ttf", ".otf"].includes(extension) || !existsSync(entry.src) || !statSync(entry.src).isFile())
|
|
@@ -152,23 +333,23 @@ function syncNativeFonts(fonts, output) {
|
|
|
152
333
|
throw new Error(`Native font license not found: ${entry.license}`);
|
|
153
334
|
return { family, ...entry, filename: `${family}${extension}` };
|
|
154
335
|
});
|
|
155
|
-
|
|
336
|
+
rmSync2(destination, { recursive: true, force: true });
|
|
156
337
|
if (!files.length)
|
|
157
338
|
return [];
|
|
158
|
-
|
|
339
|
+
mkdirSync2(destination, { recursive: true });
|
|
159
340
|
for (const file of files) {
|
|
160
|
-
cpSync(file.src,
|
|
341
|
+
cpSync(file.src, join3(destination, file.filename));
|
|
161
342
|
if (file.license)
|
|
162
|
-
cpSync(file.license,
|
|
343
|
+
cpSync(file.license, join3(destination, `${file.family}.LICENSE.txt`));
|
|
163
344
|
}
|
|
164
345
|
return files.map((file) => `NativeFonts/${file.filename}`);
|
|
165
346
|
}
|
|
166
|
-
var TEMPLATES_DIR =
|
|
347
|
+
var TEMPLATES_DIR = join3(dirname2(import.meta.dir), "templates");
|
|
167
348
|
function resolveCraftAndroidVersion(startDir = import.meta.dir) {
|
|
168
|
-
let directory =
|
|
349
|
+
let directory = resolve2(startDir);
|
|
169
350
|
while (true) {
|
|
170
351
|
try {
|
|
171
|
-
const pkg = JSON.parse(readFileSync2(
|
|
352
|
+
const pkg = JSON.parse(readFileSync2(join3(directory, "package.json"), "utf8"));
|
|
172
353
|
if ((pkg.name === "@craft-native/android" || pkg.name === "craft-native") && typeof pkg.version === "string" && pkg.version)
|
|
173
354
|
return pkg.version;
|
|
174
355
|
} catch {}
|
|
@@ -305,7 +486,7 @@ function requireRegularFile(path, label) {
|
|
|
305
486
|
}
|
|
306
487
|
function containsPath(parent, candidate) {
|
|
307
488
|
const relativePath = relative(parent, candidate);
|
|
308
|
-
return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !
|
|
489
|
+
return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute2(relativePath);
|
|
309
490
|
}
|
|
310
491
|
function validateGoogleServicesFile(path, packageName) {
|
|
311
492
|
requireRegularFile(path, "Google services file");
|
|
@@ -498,36 +679,36 @@ function validateNativeRouteConfig(config) {
|
|
|
498
679
|
}
|
|
499
680
|
}
|
|
500
681
|
function writeAndroidConfig(output, config) {
|
|
501
|
-
writeFileSync(
|
|
682
|
+
writeFileSync(join3(output, "craft.config.json"), JSON.stringify(config, null, 2));
|
|
502
683
|
const runtimeConfig = { ...config };
|
|
503
684
|
delete runtimeConfig.appIconPath;
|
|
504
685
|
delete runtimeConfig.googleServicesFile;
|
|
505
|
-
writeFileSync(
|
|
686
|
+
writeFileSync(join3(output, "app/src/main/assets/craft.config.json"), JSON.stringify(runtimeConfig, null, 2));
|
|
506
687
|
}
|
|
507
688
|
function syncAndroidWebAssets(source, output) {
|
|
508
|
-
const sourcePath =
|
|
689
|
+
const sourcePath = resolve2(source);
|
|
509
690
|
if (!existsSync(sourcePath))
|
|
510
691
|
throw new Error(`Web asset path not found: ${source}`);
|
|
511
692
|
const sourceStat = statSync(sourcePath);
|
|
512
693
|
if (!sourceStat.isDirectory() && !sourceStat.isFile()) {
|
|
513
694
|
throw new Error(`Web asset path must be a file or directory: ${source}`);
|
|
514
695
|
}
|
|
515
|
-
const assetsDir =
|
|
696
|
+
const assetsDir = resolve2(output, "app/src/main/assets");
|
|
516
697
|
if (containsPath(sourcePath, assetsDir) || containsPath(assetsDir, sourcePath)) {
|
|
517
698
|
throw new Error(`Web asset source must not overlap generated asset directory: ${source}`);
|
|
518
699
|
}
|
|
519
700
|
if (sourceStat.isDirectory()) {
|
|
520
|
-
requireRegularFile(
|
|
701
|
+
requireRegularFile(join3(sourcePath, "index.html"), "Web asset directory entry point");
|
|
521
702
|
}
|
|
522
|
-
const configPath =
|
|
703
|
+
const configPath = join3(assetsDir, "craft.config.json");
|
|
523
704
|
const config = existsSync(configPath) ? readFileSync2(configPath) : undefined;
|
|
524
|
-
|
|
525
|
-
|
|
705
|
+
rmSync2(assetsDir, { recursive: true, force: true });
|
|
706
|
+
mkdirSync2(assetsDir, { recursive: true });
|
|
526
707
|
if (sourceStat.isDirectory())
|
|
527
708
|
cpSync(sourcePath, assetsDir, { recursive: true });
|
|
528
709
|
else
|
|
529
|
-
cpSync(sourcePath,
|
|
530
|
-
if (!existsSync(
|
|
710
|
+
cpSync(sourcePath, join3(assetsDir, "index.html"));
|
|
711
|
+
if (!existsSync(join3(assetsDir, "index.html")))
|
|
531
712
|
throw new Error(`Web asset directory must contain index.html: ${source}`);
|
|
532
713
|
if (config)
|
|
533
714
|
writeFileSync(configPath, config);
|
|
@@ -571,10 +752,10 @@ function renderAndroidPermissions(config) {
|
|
|
571
752
|
`);
|
|
572
753
|
}
|
|
573
754
|
function writeNativeShellSources(output, packagePath, packageName, config) {
|
|
574
|
-
const javaDir =
|
|
755
|
+
const javaDir = join3(output, "app/src/main/java", packagePath);
|
|
575
756
|
const write = (template, file, replace = (source) => source) => {
|
|
576
|
-
const source = readFileSync2(
|
|
577
|
-
writeFileSync(
|
|
757
|
+
const source = readFileSync2(join3(TEMPLATES_DIR, template), "utf-8").replace(/\{\{PACKAGE_NAME\}\}/g, packageName);
|
|
758
|
+
writeFileSync(join3(javaDir, file), replace(source));
|
|
578
759
|
};
|
|
579
760
|
write("CraftBluetooth.kt.template", "CraftBluetooth.kt");
|
|
580
761
|
write("CraftNativeShell.kt.template", "CraftNativeShell.kt");
|
|
@@ -596,9 +777,9 @@ function writeNativeShellSources(output, packagePath, packageName, config) {
|
|
|
596
777
|
if (config.enablePushNotifications)
|
|
597
778
|
write("CraftNativeMessagingService.kt.template", "CraftNativeMessagingService.kt");
|
|
598
779
|
else
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
writeFileSync(
|
|
780
|
+
rmSync2(join3(javaDir, "CraftNativeMessagingService.kt"), { force: true });
|
|
781
|
+
mkdirSync2(join3(output, "app/src/main/res/xml"), { recursive: true });
|
|
782
|
+
writeFileSync(join3(output, "app/src/main/res/xml/craft_file_paths.xml"), `<?xml version="1.0" encoding="utf-8"?>
|
|
602
783
|
<paths>
|
|
603
784
|
<cache-path name="craft-files" path="craft-files/" />
|
|
604
785
|
<files-path name="craft-media" path="craft-media/" />
|
|
@@ -677,7 +858,7 @@ function resolveRuntimeDir(override) {
|
|
|
677
858
|
return dir;
|
|
678
859
|
}
|
|
679
860
|
function installRuntime(output, runtimeDir) {
|
|
680
|
-
const found = RUNTIME_ABIS.map((abi) => ({ abi, source:
|
|
861
|
+
const found = RUNTIME_ABIS.map((abi) => ({ abi, source: join3(runtimeDir, abi, "libcraft.so") })).filter((entry) => existsSync(entry.source));
|
|
681
862
|
if (found.length === 0) {
|
|
682
863
|
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.");
|
|
683
864
|
}
|
|
@@ -685,12 +866,12 @@ function installRuntime(output, runtimeDir) {
|
|
|
685
866
|
const missing = RUNTIME_ABIS.filter((abi) => !found.some((entry) => entry.abi === abi));
|
|
686
867
|
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.");
|
|
687
868
|
}
|
|
688
|
-
const dest =
|
|
689
|
-
|
|
869
|
+
const dest = join3(output, "app/src/main/jniLibs");
|
|
870
|
+
rmSync2(dest, { force: true, recursive: true });
|
|
690
871
|
for (const { abi, source } of found) {
|
|
691
|
-
const abiDir =
|
|
692
|
-
|
|
693
|
-
cpSync(source,
|
|
872
|
+
const abiDir = join3(dest, abi);
|
|
873
|
+
mkdirSync2(abiDir, { recursive: true });
|
|
874
|
+
cpSync(source, join3(abiDir, "libcraft.so"));
|
|
694
875
|
}
|
|
695
876
|
return true;
|
|
696
877
|
}
|
|
@@ -733,46 +914,46 @@ async function init(options) {
|
|
|
733
914
|
}
|
|
734
915
|
const dirs = [
|
|
735
916
|
output,
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
917
|
+
join3(output, "app/src/main/java", packagePath),
|
|
918
|
+
join3(output, "app/src/main/res/layout"),
|
|
919
|
+
join3(output, "app/src/main/res/values"),
|
|
920
|
+
join3(output, "app/src/main/res/values-night"),
|
|
921
|
+
join3(output, "app/src/main/res/drawable"),
|
|
922
|
+
join3(output, "app/src/main/assets"),
|
|
923
|
+
join3(output, "gradle/wrapper")
|
|
743
924
|
];
|
|
744
925
|
if (existsSync(output) && !statSync(output).isDirectory()) {
|
|
745
926
|
throw new Error(`Android project output must be a directory: ${output}`);
|
|
746
927
|
}
|
|
747
928
|
for (const dir of dirs) {
|
|
748
929
|
if (!existsSync(dir)) {
|
|
749
|
-
|
|
930
|
+
mkdirSync2(dir, { recursive: true });
|
|
750
931
|
}
|
|
751
932
|
}
|
|
752
933
|
writeAndroidConfig(output, config);
|
|
753
934
|
const hasGoogleServices = Boolean(config.googleServicesFile);
|
|
754
935
|
if (config.googleServicesFile) {
|
|
755
|
-
cpSync(config.googleServicesFile,
|
|
936
|
+
cpSync(config.googleServicesFile, join3(output, "app/google-services.json"));
|
|
756
937
|
}
|
|
757
|
-
const mainActivityTemplate = readFileSync2(
|
|
938
|
+
const mainActivityTemplate = readFileSync2(join3(TEMPLATES_DIR, config.renderer === "native" ? "MainActivityNative.kt.template" : "MainActivity.kt.template"), "utf-8");
|
|
758
939
|
const mainActivity = mainActivityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name);
|
|
759
|
-
writeFileSync(
|
|
760
|
-
writeFileSync(
|
|
761
|
-
writeFileSync(
|
|
940
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, "MainActivity.kt"), mainActivity);
|
|
941
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftAppLinks.kt"), readFileSync2(join3(TEMPLATES_DIR, "CraftAppLinks.kt.template"), "utf8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
942
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftCrashReporter.kt"), readFileSync2(join3(TEMPLATES_DIR, "CraftCrashReporter.kt.template"), "utf-8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_ANDROID_VERSION));
|
|
762
943
|
if (config.renderer === "native") {
|
|
763
|
-
const mutationTemplate = readFileSync2(
|
|
764
|
-
const flatListTemplate = readFileSync2(
|
|
765
|
-
const refreshableFlatListTemplate = readFileSync2(
|
|
766
|
-
const capabilityTemplate = readFileSync2(
|
|
767
|
-
const notificationTemplate = readFileSync2(
|
|
768
|
-
writeFileSync(
|
|
769
|
-
writeFileSync(
|
|
770
|
-
writeFileSync(
|
|
771
|
-
writeFileSync(
|
|
772
|
-
writeFileSync(
|
|
944
|
+
const mutationTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftNativeMutation.kt.template"), "utf-8");
|
|
945
|
+
const flatListTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftNativeFlatList.kt.template"), "utf-8");
|
|
946
|
+
const refreshableFlatListTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftNativeRefreshableFlatList.kt.template"), "utf-8");
|
|
947
|
+
const capabilityTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftNativeCapabilities.kt.template"), "utf-8");
|
|
948
|
+
const notificationTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftNativeNotificationReceiver.kt.template"), "utf-8");
|
|
949
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftNativeMutation.kt"), mutationTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
950
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftNativeFlatList.kt"), flatListTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
951
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftNativeRefreshableFlatList.kt"), refreshableFlatListTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
952
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftNativeCapabilities.kt"), capabilityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
953
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftNativeNotificationReceiver.kt"), notificationTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
773
954
|
writeNativeShellSources(output, packagePath, finalPackageName, config);
|
|
774
955
|
}
|
|
775
|
-
const craftBridgeTemplate = readFileSync2(
|
|
956
|
+
const craftBridgeTemplate = readFileSync2(join3(TEMPLATES_DIR, "CraftBridge.kt.template"), "utf-8");
|
|
776
957
|
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 {
|
|
777
958
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
|
778
959
|
&& ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
|
@@ -792,16 +973,16 @@ async function init(options) {
|
|
|
792
973
|
}` : `evaluatePromiseJavascript(
|
|
793
974
|
"window._craftPushReject && window._craftPushReject('Push notifications are disabled')"
|
|
794
975
|
)`);
|
|
795
|
-
writeFileSync(
|
|
796
|
-
const craftNative = readFileSync2(
|
|
797
|
-
const nativeDir =
|
|
798
|
-
|
|
799
|
-
writeFileSync(
|
|
800
|
-
const serviceTemplate = readFileSync2(
|
|
801
|
-
writeFileSync(
|
|
802
|
-
const healthTemplate = readFileSync2(
|
|
803
|
-
writeFileSync(
|
|
804
|
-
const manifestTemplate = readFileSync2(
|
|
976
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftBridge.kt"), craftBridge);
|
|
977
|
+
const craftNative = readFileSync2(join3(TEMPLATES_DIR, "CraftNative.kt.template"), "utf-8");
|
|
978
|
+
const nativeDir = join3(output, "app/src/main/java/com/craft/runtime");
|
|
979
|
+
mkdirSync2(nativeDir, { recursive: true });
|
|
980
|
+
writeFileSync(join3(nativeDir, "CraftNative.kt"), craftNative);
|
|
981
|
+
const serviceTemplate = readFileSync2(join3(TEMPLATES_DIR, "LocationRecordingService.kt.template"), "utf-8");
|
|
982
|
+
writeFileSync(join3(nativeDir, "LocationRecordingService.kt"), serviceTemplate);
|
|
983
|
+
const healthTemplate = readFileSync2(join3(TEMPLATES_DIR, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
|
|
984
|
+
writeFileSync(join3(output, "app/src/main/java", packagePath, "CraftHealthConnect.kt"), healthTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
985
|
+
const manifestTemplate = readFileSync2(join3(TEMPLATES_DIR, "AndroidManifest.xml.template"), "utf-8");
|
|
805
986
|
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://") || config.enableLocalNetwork ? "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(/\{\{NATIVE_NOTIFICATION_RECEIVER\}\}/g, config.renderer === "native" ? renderNativeShellManifest(config) : "").replace(/\{\{CRASH_REPORTER_PROVIDER\}\}/g, ` <provider android:name=".CraftCrashReporterInit" android:authorities="\${applicationId}.craft-crash" android:exported="false" android:initOrder="1000" />`).replace(/\{\{GOOGLE_MAPS_API_KEY\}\}/g, config.mapsApiKey ? ` <meta-data android:name="com.google.android.geo.API_KEY" android:value="${escapeXml(config.mapsApiKey)}" />` : "").replace(/\{\{HEALTH_CONNECT_QUERIES\}\}/g, config.enableHealthConnect ? ` <queries>
|
|
806
987
|
<package android:name="com.google.android.apps.healthdata" />
|
|
807
988
|
</queries>` : "").replace(/\{\{HEALTH_CONNECT_RATIONALE\}\}/g, config.enableHealthConnect ? ` <intent-filter>
|
|
@@ -811,28 +992,28 @@ async function init(options) {
|
|
|
811
992
|
<action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
|
|
812
993
|
<category android:name="android.intent.category.HEALTH_PERMISSIONS" />
|
|
813
994
|
</intent-filter>`}` : "");
|
|
814
|
-
writeFileSync(
|
|
815
|
-
const projectGradleTemplate = readFileSync2(
|
|
816
|
-
writeFileSync(
|
|
817
|
-
const appGradleTemplate = readFileSync2(
|
|
995
|
+
writeFileSync(join3(output, "app/src/main/AndroidManifest.xml"), manifest);
|
|
996
|
+
const projectGradleTemplate = readFileSync2(join3(TEMPLATES_DIR, "build.gradle.kts.project.template"), "utf-8");
|
|
997
|
+
writeFileSync(join3(output, "build.gradle.kts"), projectGradleTemplate.replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services") version "4.4.2" apply false' : ""));
|
|
998
|
+
const appGradleTemplate = readFileSync2(join3(TEMPLATES_DIR, "build.gradle.kts.app.template"), "utf-8");
|
|
818
999
|
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 ?? ANDROID_MIN_SDK)).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(/\{\{REFRESH_LAYOUT_DEPENDENCY\}\}/g, config.renderer === "native" ? ' implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")' : "").replace(/\{\{GOOGLE_MAPS_DEPENDENCY\}\}/g, config.renderer === "native" ? ` // Native STX Map overlays and trail routes
|
|
819
1000
|
implementation("com.google.android.gms:play-services-maps:18.2.0")` : "").replace(/\{\{HEALTH_CONNECT_DEPENDENCIES\}\}/g, config.enableHealthConnect ? ` implementation("androidx.health.connect:connect-client:1.1.0")
|
|
820
1001
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")` : "").replace(/\{\{JAVASCRIPT_ENGINE_DEPENDENCY\}\}/g, config.renderer === "native" ? ` implementation("androidx.javascriptengine:javascriptengine:1.1.0")
|
|
821
1002
|
// Native background refresh (WorkManager).
|
|
822
1003
|
implementation("androidx.work:work-runtime-ktx:2.9.1")` : "");
|
|
823
|
-
writeFileSync(
|
|
824
|
-
const proguardTemplate = readFileSync2(
|
|
825
|
-
writeFileSync(
|
|
826
|
-
const settingsTemplate = readFileSync2(
|
|
1004
|
+
writeFileSync(join3(output, "app/build.gradle.kts"), appGradle);
|
|
1005
|
+
const proguardTemplate = readFileSync2(join3(TEMPLATES_DIR, "proguard-rules.pro.template"), "utf-8");
|
|
1006
|
+
writeFileSync(join3(output, "app/proguard-rules.pro"), proguardTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
1007
|
+
const settingsTemplate = readFileSync2(join3(TEMPLATES_DIR, "settings.gradle.kts.template"), "utf-8");
|
|
827
1008
|
const settings = settingsTemplate.replace(/\{\{APP_NAME\}\}/g, escapeKotlinString(generatedGradleProjectName(name)));
|
|
828
|
-
writeFileSync(
|
|
1009
|
+
writeFileSync(join3(output, "settings.gradle.kts"), settings);
|
|
829
1010
|
const gradleProperties = `org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|
830
1011
|
android.useAndroidX=true
|
|
831
1012
|
kotlin.code.style=official
|
|
832
1013
|
android.nonTransitiveRClass=true
|
|
833
1014
|
`;
|
|
834
|
-
writeFileSync(
|
|
835
|
-
writeFileSync(
|
|
1015
|
+
writeFileSync(join3(output, "gradle.properties"), gradleProperties);
|
|
1016
|
+
writeFileSync(join3(output, "local.properties"), `# SDK location will be set by Android Studio
|
|
836
1017
|
`);
|
|
837
1018
|
const gradleWrapperProps = `distributionBase=GRADLE_USER_HOME
|
|
838
1019
|
distributionPath=wrapper/dists
|
|
@@ -842,13 +1023,13 @@ validateDistributionUrl=true
|
|
|
842
1023
|
zipStoreBase=GRADLE_USER_HOME
|
|
843
1024
|
zipStorePath=wrapper/dists
|
|
844
1025
|
`;
|
|
845
|
-
writeFileSync(
|
|
1026
|
+
writeFileSync(join3(output, "gradle/wrapper/gradle-wrapper.properties"), gradleWrapperProps);
|
|
846
1027
|
const stringsXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
847
1028
|
<resources>
|
|
848
1029
|
<string name="app_name">${escapeXml(name)}</string>
|
|
849
1030
|
</resources>
|
|
850
1031
|
`;
|
|
851
|
-
writeFileSync(
|
|
1032
|
+
writeFileSync(join3(output, "app/src/main/res/values/strings.xml"), stringsXml);
|
|
852
1033
|
const colorsXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
853
1034
|
<resources>
|
|
854
1035
|
<color name="primary">#1a1a2e</color>
|
|
@@ -857,8 +1038,8 @@ zipStorePath=wrapper/dists
|
|
|
857
1038
|
<color name="background">${config.backgroundColor}</color>
|
|
858
1039
|
</resources>
|
|
859
1040
|
`;
|
|
860
|
-
writeFileSync(
|
|
861
|
-
const darkColorsPath =
|
|
1041
|
+
writeFileSync(join3(output, "app/src/main/res/values/colors.xml"), colorsXml);
|
|
1042
|
+
const darkColorsPath = join3(output, "app/src/main/res/values-night/colors.xml");
|
|
862
1043
|
if (config.backgroundColorDark) {
|
|
863
1044
|
writeFileSync(darkColorsPath, `<?xml version="1.0" encoding="utf-8"?>
|
|
864
1045
|
<resources>
|
|
@@ -866,7 +1047,7 @@ zipStorePath=wrapper/dists
|
|
|
866
1047
|
</resources>
|
|
867
1048
|
`);
|
|
868
1049
|
} else {
|
|
869
|
-
|
|
1050
|
+
rmSync2(darkColorsPath, { force: true });
|
|
870
1051
|
}
|
|
871
1052
|
const appIconXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
872
1053
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
@@ -879,9 +1060,9 @@ zipStorePath=wrapper/dists
|
|
|
879
1060
|
</vector>
|
|
880
1061
|
`;
|
|
881
1062
|
if (config.appIconPath) {
|
|
882
|
-
cpSync(config.appIconPath,
|
|
1063
|
+
cpSync(config.appIconPath, join3(output, `app/src/main/res/drawable/craft_app_icon${appIconExtension}`));
|
|
883
1064
|
} else {
|
|
884
|
-
writeFileSync(
|
|
1065
|
+
writeFileSync(join3(output, "app/src/main/res/drawable/craft_app_icon.xml"), appIconXml);
|
|
885
1066
|
}
|
|
886
1067
|
const themesXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
887
1068
|
<resources>
|
|
@@ -892,7 +1073,7 @@ zipStorePath=wrapper/dists
|
|
|
892
1073
|
</style>
|
|
893
1074
|
</resources>
|
|
894
1075
|
`;
|
|
895
|
-
writeFileSync(
|
|
1076
|
+
writeFileSync(join3(output, "app/src/main/res/values/themes.xml"), themesXml);
|
|
896
1077
|
const activityMainXml = config.renderer === "native" ? `<?xml version="1.0" encoding="utf-8"?>
|
|
897
1078
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
898
1079
|
android:id="@+id/native_root"
|
|
@@ -912,7 +1093,7 @@ zipStorePath=wrapper/dists
|
|
|
912
1093
|
|
|
913
1094
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
914
1095
|
`;
|
|
915
|
-
writeFileSync(
|
|
1096
|
+
writeFileSync(join3(output, "app/src/main/res/layout/activity_main.xml"), activityMainXml);
|
|
916
1097
|
syncNativeFonts(config.nativeFonts, output);
|
|
917
1098
|
const placeholderHtml = `<!DOCTYPE html>
|
|
918
1099
|
<html>
|
|
@@ -951,7 +1132,7 @@ zipStorePath=wrapper/dists
|
|
|
951
1132
|
</script>
|
|
952
1133
|
</body>
|
|
953
1134
|
</html>`;
|
|
954
|
-
writeFileSync(
|
|
1135
|
+
writeFileSync(join3(output, "app/src/main/assets/index.html"), placeholderHtml);
|
|
955
1136
|
const runtimeDir = resolveRuntimeDir(options.runtimeDir);
|
|
956
1137
|
if (runtimeDir) {
|
|
957
1138
|
installRuntime(output, runtimeDir);
|
|
@@ -975,7 +1156,7 @@ async function build(options) {
|
|
|
975
1156
|
const { htmlPath, nativeBundlePath, devServer, output, release, bundle, compile = true } = options;
|
|
976
1157
|
console.log(`
|
|
977
1158
|
\uD83D\uDCE6 Building Craft Android project...`);
|
|
978
|
-
const configPath =
|
|
1159
|
+
const configPath = join3(output, "craft.config.json");
|
|
979
1160
|
if (!existsSync(configPath)) {
|
|
980
1161
|
throw new Error(`No craft.config.json found in ${output}. Run 'craft android init' first.`);
|
|
981
1162
|
}
|
|
@@ -985,13 +1166,13 @@ async function build(options) {
|
|
|
985
1166
|
if (config.renderer === "native") {
|
|
986
1167
|
if (htmlPath || devServer)
|
|
987
1168
|
throw new Error("A native Android screen cannot use --html-path or --dev-server.");
|
|
988
|
-
const destination =
|
|
989
|
-
if (nativeBundlePath &&
|
|
1169
|
+
const destination = join3(output, "app/src/main/assets/native-screen.js");
|
|
1170
|
+
if (nativeBundlePath && resolve2(nativeBundlePath) !== resolve2(destination))
|
|
990
1171
|
cpSync(nativeBundlePath, destination);
|
|
991
|
-
const icons =
|
|
1172
|
+
const icons = join3(output, "app/src/main/assets/craft-icons.json");
|
|
992
1173
|
if (options.iconsPath) {
|
|
993
1174
|
requireRegularFile(options.iconsPath, "Icon table");
|
|
994
|
-
if (
|
|
1175
|
+
if (resolve2(options.iconsPath) !== resolve2(icons))
|
|
995
1176
|
cpSync(options.iconsPath, icons);
|
|
996
1177
|
}
|
|
997
1178
|
if (!existsSync(destination)) {
|
|
@@ -1000,7 +1181,7 @@ async function build(options) {
|
|
|
1000
1181
|
} else if (nativeBundlePath) {
|
|
1001
1182
|
throw new Error('--native-bundle requires renderer: "native" in craft.config.json.');
|
|
1002
1183
|
}
|
|
1003
|
-
if (existsSync(
|
|
1184
|
+
if (existsSync(join3(output, "app/src/main/jniLibs"))) {
|
|
1004
1185
|
const runtimeDir = resolveRuntimeDir(options.runtimeDir);
|
|
1005
1186
|
if (runtimeDir) {
|
|
1006
1187
|
installRuntime(output, runtimeDir);
|
|
@@ -1028,7 +1209,7 @@ async function build(options) {
|
|
|
1028
1209
|
syncNativeFonts(config.nativeFonts, output);
|
|
1029
1210
|
if (!compile)
|
|
1030
1211
|
return;
|
|
1031
|
-
const gradlewPath =
|
|
1212
|
+
const gradlewPath = join3(output, "gradlew");
|
|
1032
1213
|
if (!existsSync(gradlewPath)) {
|
|
1033
1214
|
console.log(" Generating Gradle wrapper...");
|
|
1034
1215
|
try {
|
|
@@ -1075,13 +1256,13 @@ async function run(options) {
|
|
|
1075
1256
|
await build({ output });
|
|
1076
1257
|
console.log("\uD83D\uDCF1 Installing and running on device...");
|
|
1077
1258
|
try {
|
|
1078
|
-
const apkPath =
|
|
1259
|
+
const apkPath = join3(output, "app/build/outputs/apk/debug/app-debug.apk");
|
|
1079
1260
|
if (device) {
|
|
1080
1261
|
await $`adb -s ${device} install -r ${apkPath}`;
|
|
1081
1262
|
} else {
|
|
1082
1263
|
await $`adb install -r ${apkPath}`;
|
|
1083
1264
|
}
|
|
1084
|
-
const config = JSON.parse(readFileSync2(
|
|
1265
|
+
const config = JSON.parse(readFileSync2(join3(output, "craft.config.json"), "utf-8"));
|
|
1085
1266
|
const launchCmd = `${config.packageName}/.MainActivity`;
|
|
1086
1267
|
if (device) {
|
|
1087
1268
|
await $`adb -s ${device} shell am start -n ${launchCmd}`;
|
|
@@ -1108,6 +1289,7 @@ export {
|
|
|
1108
1289
|
installRuntime,
|
|
1109
1290
|
init,
|
|
1110
1291
|
healthConnectPermissions,
|
|
1292
|
+
createAndroidEmulator,
|
|
1111
1293
|
build,
|
|
1112
1294
|
HEALTH_CONNECT_PERMISSIONS,
|
|
1113
1295
|
ANDROID_MIN_SDK
|