craft-native 0.0.202 → 0.0.203
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 +1 -0
- package/dist/android/src/index.js +141 -85
- package/dist/android/src/testing.d.ts +2 -0
- package/dist/android/templates/MainActivityNative.kt.template +151 -55
- package/dist/android/templates/build.gradle.kts.app.template +2 -0
- package/dist/cli.js +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/ios/src/index.d.ts +1 -1
- package/dist/ios/src/index.js +17 -4
- package/dist/ios/templates/CraftHybrid.swift +28 -3
- package/dist/ios/templates/CraftNativeFiles.swift +4 -2
- package/dist/ios/templates/CraftNativeFlatList.swift +62 -8
- package/dist/ios/templates/CraftNativeScreen.swift +38 -14
- package/package.json +1 -1
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Generates native Android apps from web content using WebView.
|
|
5
5
|
*/
|
|
6
|
+
export { runKotlinFixture } from './testing.js';
|
|
6
7
|
/** Oldest Android API level supported by the generated Kotlin runtime. */
|
|
7
8
|
export declare const ANDROID_MIN_SDK = 26;
|
|
8
9
|
export interface CraftAndroidConfig {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../android/src/index.ts
|
|
3
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
|
|
4
|
-
import { dirname, extname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
3
|
+
import { cpSync, existsSync, mkdirSync, readFileSync as readFileSync2, rmSync, statSync, writeFileSync } from "fs";
|
|
4
|
+
import { dirname as dirname2, extname, isAbsolute, join as join2, relative, resolve, sep } from "path";
|
|
5
5
|
var {$ } = globalThis.Bun;
|
|
6
6
|
|
|
7
7
|
// ../android/src/promise-runtime.ts
|
|
@@ -77,17 +77,73 @@ function renderAndroidPromiseRuntime(indent = "") {
|
|
|
77
77
|
`);
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
// ../android/src/testing.ts
|
|
81
|
+
import { readFileSync } from "fs";
|
|
82
|
+
import { mkdtemp, rm, writeFile } from "fs/promises";
|
|
83
|
+
import { tmpdir } from "os";
|
|
84
|
+
import { dirname, join } from "path";
|
|
85
|
+
async function runKotlinFixture(source) {
|
|
86
|
+
const gradle = Bun.which("gradle");
|
|
87
|
+
if (!gradle)
|
|
88
|
+
throw new Error("Gradle is required to run native Kotlin fixtures");
|
|
89
|
+
const template = readFileSync(join(dirname(import.meta.dir), "templates/build.gradle.kts.project.template"), "utf8");
|
|
90
|
+
const version = template.match(/id\("org.jetbrains.kotlin.android"\) version "([\d.]+)"/)?.[1];
|
|
91
|
+
if (!version)
|
|
92
|
+
throw new Error("The Android builder Kotlin version is missing");
|
|
93
|
+
const directory = await mkdtemp(join(tmpdir(), "craft-kotlin-fixture-"));
|
|
94
|
+
try {
|
|
95
|
+
await writeFile(join(directory, "Fixture.kt"), source);
|
|
96
|
+
await writeFile(join(directory, "settings.gradle"), `rootProject.name = 'craft-kotlin-fixture'
|
|
97
|
+
`);
|
|
98
|
+
await writeFile(join(directory, "gradle.properties"), `org.gradle.jvmargs=-Xmx512m
|
|
99
|
+
`);
|
|
100
|
+
await writeFile(join(directory, "build.gradle"), `
|
|
101
|
+
repositories { mavenCentral() }
|
|
102
|
+
configurations {
|
|
103
|
+
kotlinCompiler
|
|
104
|
+
kotlinRuntime
|
|
105
|
+
}
|
|
106
|
+
dependencies {
|
|
107
|
+
kotlinCompiler 'org.jetbrains.kotlin:kotlin-compiler-embeddable:${version}'
|
|
108
|
+
kotlinRuntime 'org.jetbrains.kotlin:kotlin-stdlib:${version}'
|
|
109
|
+
}
|
|
110
|
+
tasks.register('compileFixture', JavaExec) {
|
|
111
|
+
classpath = configurations.kotlinCompiler
|
|
112
|
+
mainClass = 'org.jetbrains.kotlin.cli.jvm.K2JVMCompiler'
|
|
113
|
+
args '-no-stdlib', '-no-reflect', '-classpath', configurations.kotlinRuntime.asPath,
|
|
114
|
+
'-d', layout.buildDirectory.dir('classes').get().asFile.absolutePath,
|
|
115
|
+
file('Fixture.kt').absolutePath
|
|
116
|
+
}
|
|
117
|
+
tasks.register('runFixture', JavaExec) {
|
|
118
|
+
dependsOn 'compileFixture'
|
|
119
|
+
classpath = files(layout.buildDirectory.dir('classes')) + configurations.kotlinRuntime
|
|
120
|
+
mainClass = 'FixtureKt'
|
|
121
|
+
}
|
|
122
|
+
`);
|
|
123
|
+
const child = Bun.spawn([gradle, "--no-daemon", "--console=plain", "--max-workers=2", "runFixture"], {
|
|
124
|
+
cwd: directory,
|
|
125
|
+
stdout: "inherit",
|
|
126
|
+
stderr: "inherit"
|
|
127
|
+
});
|
|
128
|
+
const code = await child.exited;
|
|
129
|
+
if (code !== 0)
|
|
130
|
+
throw new Error(`Native Kotlin fixture failed with exit code ${code}`);
|
|
131
|
+
} finally {
|
|
132
|
+
await rm(directory, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
80
136
|
// ../android/src/index.ts
|
|
81
|
-
var TEMPLATES_DIR =
|
|
137
|
+
var TEMPLATES_DIR = join2(dirname2(import.meta.dir), "templates");
|
|
82
138
|
function resolveCraftAndroidVersion(startDir = import.meta.dir) {
|
|
83
139
|
let directory = resolve(startDir);
|
|
84
140
|
while (true) {
|
|
85
141
|
try {
|
|
86
|
-
const pkg = JSON.parse(
|
|
142
|
+
const pkg = JSON.parse(readFileSync2(join2(directory, "package.json"), "utf8"));
|
|
87
143
|
if ((pkg.name === "@craft-native/android" || pkg.name === "craft-native") && typeof pkg.version === "string" && pkg.version)
|
|
88
144
|
return pkg.version;
|
|
89
145
|
} catch {}
|
|
90
|
-
const parent =
|
|
146
|
+
const parent = dirname2(directory);
|
|
91
147
|
if (parent === directory)
|
|
92
148
|
return "0.0.0";
|
|
93
149
|
directory = parent;
|
|
@@ -226,7 +282,7 @@ function validateGoogleServicesFile(path, packageName) {
|
|
|
226
282
|
requireRegularFile(path, "Google services file");
|
|
227
283
|
let document;
|
|
228
284
|
try {
|
|
229
|
-
document = JSON.parse(
|
|
285
|
+
document = JSON.parse(readFileSync2(path, "utf8"));
|
|
230
286
|
} catch (error) {
|
|
231
287
|
throw new Error(`Google services file must contain valid JSON: ${path}`, { cause: error });
|
|
232
288
|
}
|
|
@@ -325,11 +381,11 @@ function validateAndroidConfig(config) {
|
|
|
325
381
|
}
|
|
326
382
|
}
|
|
327
383
|
function writeAndroidConfig(output, config) {
|
|
328
|
-
writeFileSync(
|
|
384
|
+
writeFileSync(join2(output, "craft.config.json"), JSON.stringify(config, null, 2));
|
|
329
385
|
const runtimeConfig = { ...config };
|
|
330
386
|
delete runtimeConfig.appIconPath;
|
|
331
387
|
delete runtimeConfig.googleServicesFile;
|
|
332
|
-
writeFileSync(
|
|
388
|
+
writeFileSync(join2(output, "app/src/main/assets/craft.config.json"), JSON.stringify(runtimeConfig, null, 2));
|
|
333
389
|
}
|
|
334
390
|
function syncAndroidWebAssets(source, output) {
|
|
335
391
|
const sourcePath = resolve(source);
|
|
@@ -344,17 +400,17 @@ function syncAndroidWebAssets(source, output) {
|
|
|
344
400
|
throw new Error(`Web asset source must not overlap generated asset directory: ${source}`);
|
|
345
401
|
}
|
|
346
402
|
if (sourceStat.isDirectory()) {
|
|
347
|
-
requireRegularFile(
|
|
403
|
+
requireRegularFile(join2(sourcePath, "index.html"), "Web asset directory entry point");
|
|
348
404
|
}
|
|
349
|
-
const configPath =
|
|
350
|
-
const config = existsSync(configPath) ?
|
|
405
|
+
const configPath = join2(assetsDir, "craft.config.json");
|
|
406
|
+
const config = existsSync(configPath) ? readFileSync2(configPath) : undefined;
|
|
351
407
|
rmSync(assetsDir, { recursive: true, force: true });
|
|
352
408
|
mkdirSync(assetsDir, { recursive: true });
|
|
353
409
|
if (sourceStat.isDirectory())
|
|
354
410
|
cpSync(sourcePath, assetsDir, { recursive: true });
|
|
355
411
|
else
|
|
356
|
-
cpSync(sourcePath,
|
|
357
|
-
if (!existsSync(
|
|
412
|
+
cpSync(sourcePath, join2(assetsDir, "index.html"));
|
|
413
|
+
if (!existsSync(join2(assetsDir, "index.html")))
|
|
358
414
|
throw new Error(`Web asset directory must contain index.html: ${source}`);
|
|
359
415
|
if (config)
|
|
360
416
|
writeFileSync(configPath, config);
|
|
@@ -398,10 +454,10 @@ function renderAndroidPermissions(config) {
|
|
|
398
454
|
`);
|
|
399
455
|
}
|
|
400
456
|
function writeNativeShellSources(output, packagePath, packageName, config) {
|
|
401
|
-
const javaDir =
|
|
457
|
+
const javaDir = join2(output, "app/src/main/java", packagePath);
|
|
402
458
|
const write = (template, file, replace = (source) => source) => {
|
|
403
|
-
const source =
|
|
404
|
-
writeFileSync(
|
|
459
|
+
const source = readFileSync2(join2(TEMPLATES_DIR, template), "utf-8").replace(/\{\{PACKAGE_NAME\}\}/g, packageName);
|
|
460
|
+
writeFileSync(join2(javaDir, file), replace(source));
|
|
405
461
|
};
|
|
406
462
|
write("CraftBluetooth.kt.template", "CraftBluetooth.kt");
|
|
407
463
|
write("CraftNativeShell.kt.template", "CraftNativeShell.kt");
|
|
@@ -422,9 +478,9 @@ function writeNativeShellSources(output, packagePath, packageName, config) {
|
|
|
422
478
|
if (config.enablePushNotifications)
|
|
423
479
|
write("CraftNativeMessagingService.kt.template", "CraftNativeMessagingService.kt");
|
|
424
480
|
else
|
|
425
|
-
rmSync(
|
|
426
|
-
mkdirSync(
|
|
427
|
-
writeFileSync(
|
|
481
|
+
rmSync(join2(javaDir, "CraftNativeMessagingService.kt"), { force: true });
|
|
482
|
+
mkdirSync(join2(output, "app/src/main/res/xml"), { recursive: true });
|
|
483
|
+
writeFileSync(join2(output, "app/src/main/res/xml/craft_file_paths.xml"), `<?xml version="1.0" encoding="utf-8"?>
|
|
428
484
|
<paths>
|
|
429
485
|
<cache-path name="craft-files" path="craft-files/" />
|
|
430
486
|
<files-path name="craft-media" path="craft-media/" />
|
|
@@ -503,7 +559,7 @@ function resolveRuntimeDir(override) {
|
|
|
503
559
|
return dir;
|
|
504
560
|
}
|
|
505
561
|
function installRuntime(output, runtimeDir) {
|
|
506
|
-
const found = RUNTIME_ABIS.map((abi) => ({ abi, source:
|
|
562
|
+
const found = RUNTIME_ABIS.map((abi) => ({ abi, source: join2(runtimeDir, abi, "libcraft.so") })).filter((entry) => existsSync(entry.source));
|
|
507
563
|
if (found.length === 0) {
|
|
508
564
|
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.");
|
|
509
565
|
}
|
|
@@ -511,12 +567,12 @@ function installRuntime(output, runtimeDir) {
|
|
|
511
567
|
const missing = RUNTIME_ABIS.filter((abi) => !found.some((entry) => entry.abi === abi));
|
|
512
568
|
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.");
|
|
513
569
|
}
|
|
514
|
-
const dest =
|
|
570
|
+
const dest = join2(output, "app/src/main/jniLibs");
|
|
515
571
|
rmSync(dest, { force: true, recursive: true });
|
|
516
572
|
for (const { abi, source } of found) {
|
|
517
|
-
const abiDir =
|
|
573
|
+
const abiDir = join2(dest, abi);
|
|
518
574
|
mkdirSync(abiDir, { recursive: true });
|
|
519
|
-
cpSync(source,
|
|
575
|
+
cpSync(source, join2(abiDir, "libcraft.so"));
|
|
520
576
|
}
|
|
521
577
|
return true;
|
|
522
578
|
}
|
|
@@ -558,13 +614,13 @@ async function init(options) {
|
|
|
558
614
|
}
|
|
559
615
|
const dirs = [
|
|
560
616
|
output,
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
617
|
+
join2(output, "app/src/main/java", packagePath),
|
|
618
|
+
join2(output, "app/src/main/res/layout"),
|
|
619
|
+
join2(output, "app/src/main/res/values"),
|
|
620
|
+
join2(output, "app/src/main/res/values-night"),
|
|
621
|
+
join2(output, "app/src/main/res/drawable"),
|
|
622
|
+
join2(output, "app/src/main/assets"),
|
|
623
|
+
join2(output, "gradle/wrapper")
|
|
568
624
|
];
|
|
569
625
|
if (existsSync(output) && !statSync(output).isDirectory()) {
|
|
570
626
|
throw new Error(`Android project output must be a directory: ${output}`);
|
|
@@ -577,27 +633,27 @@ async function init(options) {
|
|
|
577
633
|
writeAndroidConfig(output, config);
|
|
578
634
|
const hasGoogleServices = Boolean(config.googleServicesFile);
|
|
579
635
|
if (config.googleServicesFile) {
|
|
580
|
-
cpSync(config.googleServicesFile,
|
|
636
|
+
cpSync(config.googleServicesFile, join2(output, "app/google-services.json"));
|
|
581
637
|
}
|
|
582
|
-
const mainActivityTemplate =
|
|
638
|
+
const mainActivityTemplate = readFileSync2(join2(TEMPLATES_DIR, config.renderer === "native" ? "MainActivityNative.kt.template" : "MainActivity.kt.template"), "utf-8");
|
|
583
639
|
const mainActivity = mainActivityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name);
|
|
584
|
-
writeFileSync(
|
|
585
|
-
writeFileSync(
|
|
586
|
-
writeFileSync(
|
|
640
|
+
writeFileSync(join2(output, "app/src/main/java", packagePath, "MainActivity.kt"), mainActivity);
|
|
641
|
+
writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftAppLinks.kt"), readFileSync2(join2(TEMPLATES_DIR, "CraftAppLinks.kt.template"), "utf8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
642
|
+
writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftCrashReporter.kt"), readFileSync2(join2(TEMPLATES_DIR, "CraftCrashReporter.kt.template"), "utf-8").replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{CRAFT_VERSION\}\}/g, CRAFT_ANDROID_VERSION));
|
|
587
643
|
if (config.renderer === "native") {
|
|
588
|
-
const mutationTemplate =
|
|
589
|
-
const flatListTemplate =
|
|
590
|
-
const refreshableFlatListTemplate =
|
|
591
|
-
const capabilityTemplate =
|
|
592
|
-
const notificationTemplate =
|
|
593
|
-
writeFileSync(
|
|
594
|
-
writeFileSync(
|
|
595
|
-
writeFileSync(
|
|
596
|
-
writeFileSync(
|
|
597
|
-
writeFileSync(
|
|
644
|
+
const mutationTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftNativeMutation.kt.template"), "utf-8");
|
|
645
|
+
const flatListTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftNativeFlatList.kt.template"), "utf-8");
|
|
646
|
+
const refreshableFlatListTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftNativeRefreshableFlatList.kt.template"), "utf-8");
|
|
647
|
+
const capabilityTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftNativeCapabilities.kt.template"), "utf-8");
|
|
648
|
+
const notificationTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftNativeNotificationReceiver.kt.template"), "utf-8");
|
|
649
|
+
writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftNativeMutation.kt"), mutationTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
650
|
+
writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftNativeFlatList.kt"), flatListTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
651
|
+
writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftNativeRefreshableFlatList.kt"), refreshableFlatListTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
652
|
+
writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftNativeCapabilities.kt"), capabilityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
653
|
+
writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftNativeNotificationReceiver.kt"), notificationTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
598
654
|
writeNativeShellSources(output, packagePath, finalPackageName, config);
|
|
599
655
|
}
|
|
600
|
-
const craftBridgeTemplate =
|
|
656
|
+
const craftBridgeTemplate = readFileSync2(join2(TEMPLATES_DIR, "CraftBridge.kt.template"), "utf-8");
|
|
601
657
|
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 {
|
|
602
658
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
|
603
659
|
&& ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
|
@@ -617,16 +673,16 @@ async function init(options) {
|
|
|
617
673
|
}` : `evaluatePromiseJavascript(
|
|
618
674
|
"window._craftPushReject && window._craftPushReject('Push notifications are disabled')"
|
|
619
675
|
)`);
|
|
620
|
-
writeFileSync(
|
|
621
|
-
const craftNative =
|
|
622
|
-
const nativeDir =
|
|
676
|
+
writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftBridge.kt"), craftBridge);
|
|
677
|
+
const craftNative = readFileSync2(join2(TEMPLATES_DIR, "CraftNative.kt.template"), "utf-8");
|
|
678
|
+
const nativeDir = join2(output, "app/src/main/java/com/craft/runtime");
|
|
623
679
|
mkdirSync(nativeDir, { recursive: true });
|
|
624
|
-
writeFileSync(
|
|
625
|
-
const serviceTemplate =
|
|
626
|
-
writeFileSync(
|
|
627
|
-
const healthTemplate =
|
|
628
|
-
writeFileSync(
|
|
629
|
-
const manifestTemplate =
|
|
680
|
+
writeFileSync(join2(nativeDir, "CraftNative.kt"), craftNative);
|
|
681
|
+
const serviceTemplate = readFileSync2(join2(TEMPLATES_DIR, "LocationRecordingService.kt.template"), "utf-8");
|
|
682
|
+
writeFileSync(join2(nativeDir, "LocationRecordingService.kt"), serviceTemplate);
|
|
683
|
+
const healthTemplate = readFileSync2(join2(TEMPLATES_DIR, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
|
|
684
|
+
writeFileSync(join2(output, "app/src/main/java", packagePath, "CraftHealthConnect.kt"), healthTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
685
|
+
const manifestTemplate = readFileSync2(join2(TEMPLATES_DIR, "AndroidManifest.xml.template"), "utf-8");
|
|
630
686
|
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>
|
|
631
687
|
<package android:name="com.google.android.apps.healthdata" />
|
|
632
688
|
</queries>` : "").replace(/\{\{HEALTH_CONNECT_RATIONALE\}\}/g, config.enableHealthConnect ? ` <intent-filter>
|
|
@@ -636,29 +692,28 @@ async function init(options) {
|
|
|
636
692
|
<action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
|
|
637
693
|
<category android:name="android.intent.category.HEALTH_PERMISSIONS" />
|
|
638
694
|
</intent-filter>`}` : "");
|
|
639
|
-
writeFileSync(
|
|
640
|
-
const projectGradleTemplate =
|
|
641
|
-
writeFileSync(
|
|
642
|
-
const appGradleTemplate =
|
|
695
|
+
writeFileSync(join2(output, "app/src/main/AndroidManifest.xml"), manifest);
|
|
696
|
+
const projectGradleTemplate = readFileSync2(join2(TEMPLATES_DIR, "build.gradle.kts.project.template"), "utf-8");
|
|
697
|
+
writeFileSync(join2(output, "build.gradle.kts"), projectGradleTemplate.replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services") version "4.4.2" apply false' : ""));
|
|
698
|
+
const appGradleTemplate = readFileSync2(join2(TEMPLATES_DIR, "build.gradle.kts.app.template"), "utf-8");
|
|
643
699
|
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
|
|
644
700
|
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")
|
|
645
701
|
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")
|
|
646
|
-
//
|
|
647
|
-
implementation("androidx.browser:browser:1.8.0")
|
|
702
|
+
// Native background refresh (WorkManager).
|
|
648
703
|
implementation("androidx.work:work-runtime-ktx:2.9.1")` : "");
|
|
649
|
-
writeFileSync(
|
|
650
|
-
const proguardTemplate =
|
|
651
|
-
writeFileSync(
|
|
652
|
-
const settingsTemplate =
|
|
704
|
+
writeFileSync(join2(output, "app/build.gradle.kts"), appGradle);
|
|
705
|
+
const proguardTemplate = readFileSync2(join2(TEMPLATES_DIR, "proguard-rules.pro.template"), "utf-8");
|
|
706
|
+
writeFileSync(join2(output, "app/proguard-rules.pro"), proguardTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
707
|
+
const settingsTemplate = readFileSync2(join2(TEMPLATES_DIR, "settings.gradle.kts.template"), "utf-8");
|
|
653
708
|
const settings = settingsTemplate.replace(/\{\{APP_NAME\}\}/g, escapeKotlinString(generatedGradleProjectName(name)));
|
|
654
|
-
writeFileSync(
|
|
709
|
+
writeFileSync(join2(output, "settings.gradle.kts"), settings);
|
|
655
710
|
const gradleProperties = `org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|
656
711
|
android.useAndroidX=true
|
|
657
712
|
kotlin.code.style=official
|
|
658
713
|
android.nonTransitiveRClass=true
|
|
659
714
|
`;
|
|
660
|
-
writeFileSync(
|
|
661
|
-
writeFileSync(
|
|
715
|
+
writeFileSync(join2(output, "gradle.properties"), gradleProperties);
|
|
716
|
+
writeFileSync(join2(output, "local.properties"), `# SDK location will be set by Android Studio
|
|
662
717
|
`);
|
|
663
718
|
const gradleWrapperProps = `distributionBase=GRADLE_USER_HOME
|
|
664
719
|
distributionPath=wrapper/dists
|
|
@@ -668,13 +723,13 @@ validateDistributionUrl=true
|
|
|
668
723
|
zipStoreBase=GRADLE_USER_HOME
|
|
669
724
|
zipStorePath=wrapper/dists
|
|
670
725
|
`;
|
|
671
|
-
writeFileSync(
|
|
726
|
+
writeFileSync(join2(output, "gradle/wrapper/gradle-wrapper.properties"), gradleWrapperProps);
|
|
672
727
|
const stringsXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
673
728
|
<resources>
|
|
674
729
|
<string name="app_name">${escapeXml(name)}</string>
|
|
675
730
|
</resources>
|
|
676
731
|
`;
|
|
677
|
-
writeFileSync(
|
|
732
|
+
writeFileSync(join2(output, "app/src/main/res/values/strings.xml"), stringsXml);
|
|
678
733
|
const colorsXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
679
734
|
<resources>
|
|
680
735
|
<color name="primary">#1a1a2e</color>
|
|
@@ -683,8 +738,8 @@ zipStorePath=wrapper/dists
|
|
|
683
738
|
<color name="background">${config.backgroundColor}</color>
|
|
684
739
|
</resources>
|
|
685
740
|
`;
|
|
686
|
-
writeFileSync(
|
|
687
|
-
const darkColorsPath =
|
|
741
|
+
writeFileSync(join2(output, "app/src/main/res/values/colors.xml"), colorsXml);
|
|
742
|
+
const darkColorsPath = join2(output, "app/src/main/res/values-night/colors.xml");
|
|
688
743
|
if (config.backgroundColorDark) {
|
|
689
744
|
writeFileSync(darkColorsPath, `<?xml version="1.0" encoding="utf-8"?>
|
|
690
745
|
<resources>
|
|
@@ -705,9 +760,9 @@ zipStorePath=wrapper/dists
|
|
|
705
760
|
</vector>
|
|
706
761
|
`;
|
|
707
762
|
if (config.appIconPath) {
|
|
708
|
-
cpSync(config.appIconPath,
|
|
763
|
+
cpSync(config.appIconPath, join2(output, `app/src/main/res/drawable/craft_app_icon${appIconExtension}`));
|
|
709
764
|
} else {
|
|
710
|
-
writeFileSync(
|
|
765
|
+
writeFileSync(join2(output, "app/src/main/res/drawable/craft_app_icon.xml"), appIconXml);
|
|
711
766
|
}
|
|
712
767
|
const themesXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
713
768
|
<resources>
|
|
@@ -718,7 +773,7 @@ zipStorePath=wrapper/dists
|
|
|
718
773
|
</style>
|
|
719
774
|
</resources>
|
|
720
775
|
`;
|
|
721
|
-
writeFileSync(
|
|
776
|
+
writeFileSync(join2(output, "app/src/main/res/values/themes.xml"), themesXml);
|
|
722
777
|
const activityMainXml = config.renderer === "native" ? `<?xml version="1.0" encoding="utf-8"?>
|
|
723
778
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
724
779
|
android:id="@+id/native_root"
|
|
@@ -738,7 +793,7 @@ zipStorePath=wrapper/dists
|
|
|
738
793
|
|
|
739
794
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
740
795
|
`;
|
|
741
|
-
writeFileSync(
|
|
796
|
+
writeFileSync(join2(output, "app/src/main/res/layout/activity_main.xml"), activityMainXml);
|
|
742
797
|
const placeholderHtml = `<!DOCTYPE html>
|
|
743
798
|
<html>
|
|
744
799
|
<head>
|
|
@@ -776,7 +831,7 @@ zipStorePath=wrapper/dists
|
|
|
776
831
|
</script>
|
|
777
832
|
</body>
|
|
778
833
|
</html>`;
|
|
779
|
-
writeFileSync(
|
|
834
|
+
writeFileSync(join2(output, "app/src/main/assets/index.html"), placeholderHtml);
|
|
780
835
|
const runtimeDir = resolveRuntimeDir(options.runtimeDir);
|
|
781
836
|
if (runtimeDir) {
|
|
782
837
|
installRuntime(output, runtimeDir);
|
|
@@ -800,19 +855,19 @@ async function build(options) {
|
|
|
800
855
|
const { htmlPath, nativeBundlePath, devServer, output, release, bundle, compile = true } = options;
|
|
801
856
|
console.log(`
|
|
802
857
|
\uD83D\uDCE6 Building Craft Android project...`);
|
|
803
|
-
const configPath =
|
|
858
|
+
const configPath = join2(output, "craft.config.json");
|
|
804
859
|
if (!existsSync(configPath)) {
|
|
805
860
|
throw new Error(`No craft.config.json found in ${output}. Run 'craft android init' first.`);
|
|
806
861
|
}
|
|
807
|
-
const config = JSON.parse(
|
|
862
|
+
const config = JSON.parse(readFileSync2(configPath, "utf-8"));
|
|
808
863
|
validateAndroidConfig(config);
|
|
809
864
|
if (config.renderer === "native") {
|
|
810
865
|
if (htmlPath || devServer)
|
|
811
866
|
throw new Error("A native Android screen cannot use --html-path or --dev-server.");
|
|
812
|
-
const destination =
|
|
867
|
+
const destination = join2(output, "app/src/main/assets/native-screen.js");
|
|
813
868
|
if (nativeBundlePath && resolve(nativeBundlePath) !== resolve(destination))
|
|
814
869
|
cpSync(nativeBundlePath, destination);
|
|
815
|
-
const icons =
|
|
870
|
+
const icons = join2(output, "app/src/main/assets/craft-icons.json");
|
|
816
871
|
if (options.iconsPath) {
|
|
817
872
|
requireRegularFile(options.iconsPath, "Icon table");
|
|
818
873
|
if (resolve(options.iconsPath) !== resolve(icons))
|
|
@@ -824,7 +879,7 @@ async function build(options) {
|
|
|
824
879
|
} else if (nativeBundlePath) {
|
|
825
880
|
throw new Error('--native-bundle requires renderer: "native" in craft.config.json.');
|
|
826
881
|
}
|
|
827
|
-
if (existsSync(
|
|
882
|
+
if (existsSync(join2(output, "app/src/main/jniLibs"))) {
|
|
828
883
|
const runtimeDir = resolveRuntimeDir(options.runtimeDir);
|
|
829
884
|
if (runtimeDir) {
|
|
830
885
|
installRuntime(output, runtimeDir);
|
|
@@ -851,7 +906,7 @@ async function build(options) {
|
|
|
851
906
|
}
|
|
852
907
|
if (!compile)
|
|
853
908
|
return;
|
|
854
|
-
const gradlewPath =
|
|
909
|
+
const gradlewPath = join2(output, "gradlew");
|
|
855
910
|
if (!existsSync(gradlewPath)) {
|
|
856
911
|
console.log(" Generating Gradle wrapper...");
|
|
857
912
|
try {
|
|
@@ -898,13 +953,13 @@ async function run(options) {
|
|
|
898
953
|
await build({ output });
|
|
899
954
|
console.log("\uD83D\uDCF1 Installing and running on device...");
|
|
900
955
|
try {
|
|
901
|
-
const apkPath =
|
|
956
|
+
const apkPath = join2(output, "app/build/outputs/apk/debug/app-debug.apk");
|
|
902
957
|
if (device) {
|
|
903
958
|
await $`adb -s ${device} install -r ${apkPath}`;
|
|
904
959
|
} else {
|
|
905
960
|
await $`adb install -r ${apkPath}`;
|
|
906
961
|
}
|
|
907
|
-
const config = JSON.parse(
|
|
962
|
+
const config = JSON.parse(readFileSync2(join2(output, "craft.config.json"), "utf-8"));
|
|
908
963
|
const launchCmd = `${config.packageName}/.MainActivity`;
|
|
909
964
|
if (device) {
|
|
910
965
|
await $`adb -s ${device} shell am start -n ${launchCmd}`;
|
|
@@ -921,6 +976,7 @@ async function run(options) {
|
|
|
921
976
|
}
|
|
922
977
|
export {
|
|
923
978
|
syncAndroidWebAssets,
|
|
979
|
+
runKotlinFixture,
|
|
924
980
|
run,
|
|
925
981
|
resolveRuntimeDir,
|
|
926
982
|
renderAndroidPermissions,
|