craft-native 0.0.104 → 0.0.107
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 +2 -0
- package/dist/android/src/index.js +33 -6
- package/dist/android/templates/MainActivityNative.kt.template +390 -0
- package/dist/android/templates/build.gradle.kts.app.template +1 -0
- package/dist/api/window.d.ts +6 -11
- package/dist/cli.js +198 -1507
- package/dist/index.cjs +115 -26
- package/dist/index.js +115 -26
- package/dist/ios/src/index.d.ts +4 -0
- package/dist/ios/src/index.js +32 -3
- package/dist/ios/templates/CraftApp.swift +20 -92
- package/dist/ios/templates/CraftNativeActions.swift +93 -0
- package/dist/ios/templates/CraftNativeScreen.swift +381 -0
- package/dist/package.d.ts +21 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -20,6 +20,7 @@ var __require = import.meta.require;
|
|
|
20
20
|
// src/package.ts
|
|
21
21
|
var exports_package = {};
|
|
22
22
|
__export(exports_package, {
|
|
23
|
+
windowsNotificationAppId: () => windowsNotificationAppId,
|
|
23
24
|
windowsExecutableName: () => windowsExecutableName,
|
|
24
25
|
windowsArchitecture: () => windowsArchitecture,
|
|
25
26
|
urlTypesEntry: () => urlTypesEntry,
|
|
@@ -32,11 +33,14 @@ __export(exports_package, {
|
|
|
32
33
|
pack: () => pack,
|
|
33
34
|
notarytoolArguments: () => notarytoolArguments,
|
|
34
35
|
macOSInfoPlist: () => macOSInfoPlist,
|
|
36
|
+
linuxDesktopEntry: () => linuxDesktopEntry,
|
|
35
37
|
formatPackagingCommandError: () => formatPackagingCommandError,
|
|
36
38
|
dmgCreateArguments: () => dmgCreateArguments,
|
|
37
39
|
dmgCapacityMegabytes: () => dmgCapacityMegabytes,
|
|
38
40
|
codesignArguments: () => codesignArguments,
|
|
39
|
-
candleArguments: () => candleArguments
|
|
41
|
+
candleArguments: () => candleArguments,
|
|
42
|
+
WINDOWS_NOTIFICATION_ID_FILE: () => WINDOWS_NOTIFICATION_ID_FILE,
|
|
43
|
+
DEFAULT_DEB_DEPENDENCIES: () => DEFAULT_DEB_DEPENDENCIES
|
|
40
44
|
});
|
|
41
45
|
import { spawn } from "child_process";
|
|
42
46
|
import { createHash } from "crypto";
|
|
@@ -277,6 +281,9 @@ async function packageWindows(config, outDir) {
|
|
|
277
281
|
outputPath: join(outDir, `${name}-${version2}.msi`),
|
|
278
282
|
manufacturer: config.author || "Unknown",
|
|
279
283
|
architecture: opts.architecture || windowsArchitecture(process.arch),
|
|
284
|
+
additionalFiles: opts.additionalFiles,
|
|
285
|
+
urlSchemes: opts.urlSchemes,
|
|
286
|
+
launchUrl: opts.launchUrl,
|
|
280
287
|
certificatePath: opts.certificatePath,
|
|
281
288
|
certificatePassword: opts.certificatePassword
|
|
282
289
|
});
|
|
@@ -292,7 +299,9 @@ async function packageWindows(config, outDir) {
|
|
|
292
299
|
const zipResult = await createZIP({
|
|
293
300
|
name,
|
|
294
301
|
version: version2,
|
|
302
|
+
manufacturer: config.author || "Unknown",
|
|
295
303
|
binaryPath: config.binaryPath,
|
|
304
|
+
additionalFiles: opts.additionalFiles,
|
|
296
305
|
outputPath: join(outDir, `${name}-${version2}-windows.zip`)
|
|
297
306
|
});
|
|
298
307
|
results.push({
|
|
@@ -317,7 +326,9 @@ async function packageLinux(config, outDir) {
|
|
|
317
326
|
outputPath: join(outDir, `${name}_${version2}_amd64.deb`),
|
|
318
327
|
description: config.description || "",
|
|
319
328
|
maintainer: config.author || "Unknown",
|
|
320
|
-
dependencies: opts.debDependencies || [
|
|
329
|
+
dependencies: opts.debDependencies || [...DEFAULT_DEB_DEPENDENCIES],
|
|
330
|
+
urlSchemes: opts.urlSchemes,
|
|
331
|
+
launchUrl: opts.launchUrl
|
|
321
332
|
});
|
|
322
333
|
results.push({
|
|
323
334
|
success: debResult.success,
|
|
@@ -363,17 +374,7 @@ async function packageLinux(config, outDir) {
|
|
|
363
374
|
return results;
|
|
364
375
|
}
|
|
365
376
|
function urlTypesEntry(bundleId, schemes) {
|
|
366
|
-
const
|
|
367
|
-
const unique = [];
|
|
368
|
-
for (const scheme of schemes) {
|
|
369
|
-
if (!URL_SCHEME.test(scheme))
|
|
370
|
-
throw new Error(`Invalid URL scheme ${JSON.stringify(scheme)}: must start with a letter and contain only letters, digits, "+", "-" or "."`);
|
|
371
|
-
const key = scheme.toLowerCase();
|
|
372
|
-
if (seen.has(key))
|
|
373
|
-
continue;
|
|
374
|
-
seen.add(key);
|
|
375
|
-
unique.push(scheme);
|
|
376
|
-
}
|
|
377
|
+
const unique = uniqueUrlSchemes(schemes);
|
|
377
378
|
if (unique.length === 0)
|
|
378
379
|
return null;
|
|
379
380
|
const list = unique.map((scheme) => ` <string>${xml(scheme)}</string>`).join(`
|
|
@@ -392,6 +393,46 @@ ${list}
|
|
|
392
393
|
</dict>
|
|
393
394
|
</array>`;
|
|
394
395
|
}
|
|
396
|
+
function uniqueUrlSchemes(schemes) {
|
|
397
|
+
const seen = new Set;
|
|
398
|
+
const unique = [];
|
|
399
|
+
for (const scheme of schemes) {
|
|
400
|
+
if (!URL_SCHEME.test(scheme))
|
|
401
|
+
throw new Error(`Invalid URL scheme ${JSON.stringify(scheme)}: must start with a letter and contain only letters, digits, "+", "-" or "."`);
|
|
402
|
+
const key = scheme.toLowerCase();
|
|
403
|
+
if (seen.has(key))
|
|
404
|
+
continue;
|
|
405
|
+
seen.add(key);
|
|
406
|
+
unique.push(scheme);
|
|
407
|
+
}
|
|
408
|
+
return unique;
|
|
409
|
+
}
|
|
410
|
+
function launchPageUrl(value) {
|
|
411
|
+
let url;
|
|
412
|
+
try {
|
|
413
|
+
url = new URL(value);
|
|
414
|
+
} catch {
|
|
415
|
+
throw new Error(`Invalid packaged app launch URL: ${JSON.stringify(value)}`);
|
|
416
|
+
}
|
|
417
|
+
if (!["http:", "https:", "file:"].includes(url.protocol))
|
|
418
|
+
throw new Error(`Unsupported packaged app launch URL protocol: ${url.protocol}`);
|
|
419
|
+
return url.href;
|
|
420
|
+
}
|
|
421
|
+
function desktopExecArgument(value) {
|
|
422
|
+
return `"${value.replaceAll("%", "%%").replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("$", "\\$").replaceAll("`", "\\`")}"`;
|
|
423
|
+
}
|
|
424
|
+
function linuxDesktopEntry(name, binaryName, urlSchemes = [], launchUrl) {
|
|
425
|
+
const schemes = uniqueUrlSchemes(urlSchemes);
|
|
426
|
+
const page = launchUrl ? ` --url ${desktopExecArgument(launchPageUrl(launchUrl))}` : "";
|
|
427
|
+
return `[Desktop Entry]
|
|
428
|
+
Type=Application
|
|
429
|
+
Name=${name}
|
|
430
|
+
Exec=/usr/bin/${binaryName}${page}${schemes.length ? " --deep-link %u" : ""}
|
|
431
|
+
Terminal=false
|
|
432
|
+
Categories=Utility;
|
|
433
|
+
${schemes.length ? `MimeType=${schemes.map((scheme) => `x-scheme-handler/${scheme.toLowerCase()};`).join("")}
|
|
434
|
+
` : ""}`;
|
|
435
|
+
}
|
|
395
436
|
function macOSInfoPlist(metadata) {
|
|
396
437
|
const entries = [
|
|
397
438
|
["CFBundleExecutable", metadata.name],
|
|
@@ -429,7 +470,7 @@ function codesignArguments(opts) {
|
|
|
429
470
|
"--force",
|
|
430
471
|
"--sign",
|
|
431
472
|
opts.identity,
|
|
432
|
-
"--timestamp",
|
|
473
|
+
...opts.identity === "-" ? [] : ["--timestamp"],
|
|
433
474
|
...opts.hardenedRuntime ? ["--options", "runtime"] : [],
|
|
434
475
|
...opts.entitlements ? ["--entitlements", opts.entitlements] : [],
|
|
435
476
|
"--deep",
|
|
@@ -654,6 +695,9 @@ function deterministicGuid(value) {
|
|
|
654
695
|
const hex = digest.join("").toUpperCase();
|
|
655
696
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
656
697
|
}
|
|
698
|
+
function windowsNotificationAppId(name, manufacturer) {
|
|
699
|
+
return `Craft.${deterministicGuid(`${manufacturer.trim() || "Unknown"}/${name}`)}`;
|
|
700
|
+
}
|
|
657
701
|
function windowsArchitecture(architecture) {
|
|
658
702
|
if (architecture === "ia32" || architecture === "x86")
|
|
659
703
|
return "x86";
|
|
@@ -661,7 +705,7 @@ function windowsArchitecture(architecture) {
|
|
|
661
705
|
return architecture;
|
|
662
706
|
throw new Error(`Unsupported Windows package architecture: ${architecture}`);
|
|
663
707
|
}
|
|
664
|
-
function renderWixSource(opts, sourceName) {
|
|
708
|
+
function renderWixSource(opts, sourceName, additionalFileNames = []) {
|
|
665
709
|
if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:\.[0-9]+)?$/.test(opts.version))
|
|
666
710
|
throw new Error(`MSI version must have 3 or 4 numeric parts: ${opts.version}`);
|
|
667
711
|
const id = wixIdentifier(opts.name);
|
|
@@ -669,6 +713,9 @@ function renderWixSource(opts, sourceName) {
|
|
|
669
713
|
const upgradeCode = deterministicGuid(`${manufacturer}/${opts.name}`);
|
|
670
714
|
const programFilesFolder = opts.architecture === "x86" ? "ProgramFilesFolder" : "ProgramFiles64Folder";
|
|
671
715
|
const win64 = opts.architecture === "x86" ? "no" : "yes";
|
|
716
|
+
const notificationAppId = windowsNotificationAppId(opts.name, manufacturer);
|
|
717
|
+
const schemes = uniqueUrlSchemes(opts.urlSchemes || []);
|
|
718
|
+
const page = opts.launchUrl ? ` --url "${xml(launchPageUrl(opts.launchUrl))}"` : "";
|
|
672
719
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
673
720
|
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
|
674
721
|
<Product Id="*" Name="${xml(opts.name)}" Language="1033" Version="${opts.version}" Manufacturer="${xml(manufacturer)}" UpgradeCode="${upgradeCode}">
|
|
@@ -679,18 +726,59 @@ function renderWixSource(opts, sourceName) {
|
|
|
679
726
|
<Directory Id="${programFilesFolder}">
|
|
680
727
|
<Directory Id="INSTALLFOLDER" Name="${xml(opts.name)}">
|
|
681
728
|
<Component Id="${id}Executable" Guid="*" Win64="${win64}">
|
|
682
|
-
<File Id="${id}File" Source="${xml(sourceName)}" KeyPath="yes"
|
|
729
|
+
<File Id="${id}File" Source="${xml(sourceName)}" KeyPath="yes">
|
|
730
|
+
<Shortcut Id="${id}StartMenuShortcut" Directory="ProgramMenuFolder" Name="${xml(opts.name)}" WorkingDirectory="INSTALLFOLDER"${page ? ` Arguments="${page.trim()}"` : ""}>
|
|
731
|
+
<ShortcutProperty Key="System.AppUserModel.ID" Value="${notificationAppId}" />
|
|
732
|
+
</Shortcut>
|
|
733
|
+
</File>
|
|
734
|
+
</Component>
|
|
735
|
+
<Component Id="${id}NotificationIdentity" Guid="*" Win64="${win64}">
|
|
736
|
+
<File Id="${id}NotificationIdentityFile" Source="${WINDOWS_NOTIFICATION_ID_FILE}" KeyPath="yes" />
|
|
683
737
|
</Component>
|
|
738
|
+
${additionalFileNames.map((name, index) => ` <Component Id="${id}Extra${index}" Guid="*" Win64="${win64}">
|
|
739
|
+
<File Id="${id}ExtraFile${index}" Source="${xml(name)}" KeyPath="yes" />
|
|
740
|
+
</Component>`).join(`
|
|
741
|
+
`)}
|
|
742
|
+
${schemes.map((scheme, index) => ` <Component Id="${id}Protocol${index}" Guid="${deterministicGuid(`${manufacturer}/${opts.name}/protocol/${scheme.toLowerCase()}`)}" Win64="${win64}">
|
|
743
|
+
<RegistryKey Root="HKLM" Key="Software\\Classes\\${xml(scheme.toLowerCase())}" Action="createAndRemoveOnUninstall">
|
|
744
|
+
<RegistryValue Type="string" Value="URL:${xml(opts.name)} Protocol" KeyPath="yes" />
|
|
745
|
+
<RegistryValue Type="string" Name="URL Protocol" Value="" />
|
|
746
|
+
<RegistryKey Key="shell\\open\\command">
|
|
747
|
+
<RegistryValue Type="string" Value=""[#${id}File]"${page} --deep-link "%1"" />
|
|
748
|
+
</RegistryKey>
|
|
749
|
+
</RegistryKey>
|
|
750
|
+
</Component>`).join(`
|
|
751
|
+
`)}
|
|
684
752
|
</Directory>
|
|
685
753
|
</Directory>
|
|
754
|
+
<Directory Id="ProgramMenuFolder" />
|
|
686
755
|
</Directory>
|
|
687
756
|
<Feature Id="ProductFeature" Title="${xml(opts.name)}" Level="1">
|
|
688
757
|
<ComponentRef Id="${id}Executable" />
|
|
758
|
+
<ComponentRef Id="${id}NotificationIdentity" />
|
|
759
|
+
${additionalFileNames.map((_, index) => ` <ComponentRef Id="${id}Extra${index}" />`).join(`
|
|
760
|
+
`)}
|
|
761
|
+
${schemes.map((_, index) => ` <ComponentRef Id="${id}Protocol${index}" />`).join(`
|
|
762
|
+
`)}
|
|
689
763
|
</Feature>
|
|
690
764
|
</Product>
|
|
691
765
|
</Wix>
|
|
692
766
|
`;
|
|
693
767
|
}
|
|
768
|
+
function windowsCompanionFiles(paths, executableName) {
|
|
769
|
+
const seen = new Set([executableName.toLowerCase(), WINDOWS_NOTIFICATION_ID_FILE.toLowerCase()]);
|
|
770
|
+
return (paths || []).map((path2) => {
|
|
771
|
+
const name = basename(path2);
|
|
772
|
+
if (!name || /[<>:"/\\|?*\u0000-\u001F]/.test(name) || /[. ]$/.test(name) || /^(?:CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/i.test(name))
|
|
773
|
+
throw new Error(`Invalid Windows companion file name: ${JSON.stringify(name)}`);
|
|
774
|
+
if (seen.has(name.toLowerCase()))
|
|
775
|
+
throw new Error(`Duplicate Windows package file: ${name}`);
|
|
776
|
+
seen.add(name.toLowerCase());
|
|
777
|
+
if (!existsSync3(path2) || lstatSync(path2).isDirectory())
|
|
778
|
+
throw new Error(`Windows companion file not found: ${path2}`);
|
|
779
|
+
return { path: path2, name };
|
|
780
|
+
});
|
|
781
|
+
}
|
|
694
782
|
function windowsExecutableName(name) {
|
|
695
783
|
if (name.length === 0 || name.trim() !== name || /[<>:"/\\|?*\u0000-\u001F]/.test(name) || /[. ]$/.test(name))
|
|
696
784
|
throw new Error(`Invalid Windows application name: ${JSON.stringify(name)}`);
|
|
@@ -712,11 +800,15 @@ async function createMSI(opts) {
|
|
|
712
800
|
const tempDir = mkdtempSync(join(tmpdir(), "craft-msi-"));
|
|
713
801
|
try {
|
|
714
802
|
const binaryName = windowsExecutableName(opts.name);
|
|
803
|
+
const additionalFiles = windowsCompanionFiles(opts.additionalFiles, binaryName);
|
|
715
804
|
const sourcePath = join(tempDir, binaryName);
|
|
716
805
|
const wxsPath = join(tempDir, "installer.wxs");
|
|
717
806
|
const wixobjPath = join(tempDir, "installer.wixobj");
|
|
718
807
|
copyFileSync(opts.binaryPath, sourcePath);
|
|
719
|
-
writeFileSync2(
|
|
808
|
+
writeFileSync2(join(tempDir, WINDOWS_NOTIFICATION_ID_FILE), windowsNotificationAppId(opts.name, opts.manufacturer));
|
|
809
|
+
for (const file of additionalFiles)
|
|
810
|
+
copyFileSync(file.path, join(tempDir, file.name));
|
|
811
|
+
writeFileSync2(wxsPath, renderWixSource(opts, binaryName, additionalFiles.map((file) => file.name)));
|
|
720
812
|
await runCommand("candle.exe", candleArguments(opts.architecture, wixobjPath, wxsPath), tempDir);
|
|
721
813
|
await runCommand("light.exe", ["-nologo", "-sval", "-out", opts.outputPath, wixobjPath], tempDir);
|
|
722
814
|
if (opts.certificatePath) {
|
|
@@ -738,8 +830,13 @@ async function createMSI(opts) {
|
|
|
738
830
|
}
|
|
739
831
|
async function createZIP(opts) {
|
|
740
832
|
try {
|
|
741
|
-
const
|
|
742
|
-
const
|
|
833
|
+
const binaryName = windowsExecutableName(opts.name);
|
|
834
|
+
const additionalFiles = windowsCompanionFiles(opts.additionalFiles, binaryName);
|
|
835
|
+
const entries = [{ name: binaryName, data: new Uint8Array(readFileSync2(opts.binaryPath)) }];
|
|
836
|
+
entries.push({ name: WINDOWS_NOTIFICATION_ID_FILE, data: new TextEncoder().encode(windowsNotificationAppId(opts.name, opts.manufacturer)) });
|
|
837
|
+
for (const file of additionalFiles)
|
|
838
|
+
entries.push({ name: file.name, data: new Uint8Array(readFileSync2(file.path)) });
|
|
839
|
+
const zip = buildZip(entries);
|
|
743
840
|
writeFileSync2(opts.outputPath, zip);
|
|
744
841
|
return { success: true, outputPath: opts.outputPath };
|
|
745
842
|
} catch (err) {
|
|
@@ -769,6 +866,7 @@ async function createDEB(opts) {
|
|
|
769
866
|
return;
|
|
770
867
|
}
|
|
771
868
|
}
|
|
869
|
+
const desktopContent = linuxDesktopEntry(opts.name, sanitizedName, opts.urlSchemes, opts.launchUrl);
|
|
772
870
|
const tempDir = mkdtempSync(join(tmpdir(), "craft-deb-"));
|
|
773
871
|
const debianDir = join(tempDir, "DEBIAN");
|
|
774
872
|
const binDir = join(tempDir, "usr", "bin");
|
|
@@ -789,13 +887,6 @@ Maintainer: ${maintainer}
|
|
|
789
887
|
Description: ${description || opts.name}
|
|
790
888
|
`;
|
|
791
889
|
writeFileSync2(join(debianDir, "control"), controlContent);
|
|
792
|
-
const desktopContent = `[Desktop Entry]
|
|
793
|
-
Type=Application
|
|
794
|
-
Name=${opts.name}
|
|
795
|
-
Exec=/usr/bin/${binaryName}
|
|
796
|
-
Terminal=false
|
|
797
|
-
Categories=Utility;
|
|
798
|
-
`;
|
|
799
890
|
writeFileSync2(join(applicationsDir, `${binaryName}.desktop`), desktopContent);
|
|
800
891
|
const proc = spawn("dpkg-deb", ["--build", tempDir, opts.outputPath]);
|
|
801
892
|
proc.on("close", (code) => {
|
|
@@ -1031,8 +1122,9 @@ async function pack(options) {
|
|
|
1031
1122
|
platforms: [detectPlatform()]
|
|
1032
1123
|
});
|
|
1033
1124
|
}
|
|
1034
|
-
var CRC32_TABLE, URL_SCHEME, MEBIBYTE, HDIUTIL_MAX_ATTEMPTS = 3, runHdiutil = (args) => runTool("hdiutil", args);
|
|
1125
|
+
var DEFAULT_DEB_DEPENDENCIES, CRC32_TABLE, URL_SCHEME, MEBIBYTE, HDIUTIL_MAX_ATTEMPTS = 3, runHdiutil = (args) => runTool("hdiutil", args), WINDOWS_NOTIFICATION_ID_FILE = "craft-notification-id.txt";
|
|
1035
1126
|
var init_package = __esm(() => {
|
|
1127
|
+
DEFAULT_DEB_DEPENDENCIES = ["libgtk-3-0", "libwebkit2gtk-4.1-37", "libnotify-bin"];
|
|
1036
1128
|
CRC32_TABLE = (() => {
|
|
1037
1129
|
const table = new Uint32Array(256);
|
|
1038
1130
|
for (let n = 0;n < 256; n++) {
|
|
@@ -1047,1416 +1139,6 @@ var init_package = __esm(() => {
|
|
|
1047
1139
|
MEBIBYTE = 1024 * 1024;
|
|
1048
1140
|
});
|
|
1049
1141
|
|
|
1050
|
-
// dist/ios/src/index.js
|
|
1051
|
-
var exports_src = {};
|
|
1052
|
-
__export(exports_src, {
|
|
1053
|
-
syncWebAssets: () => syncWebAssets,
|
|
1054
|
-
showSimulator: () => showSimulator,
|
|
1055
|
-
run: () => run,
|
|
1056
|
-
resolveRuntimeDir: () => resolveRuntimeDir,
|
|
1057
|
-
resolveAppearance: () => resolveAppearance,
|
|
1058
|
-
renderWatchEntitlements: () => renderWatchEntitlements,
|
|
1059
|
-
renderUsageDescriptions: () => renderUsageDescriptions,
|
|
1060
|
-
renderUrlTypes: () => renderUrlTypes,
|
|
1061
|
-
renderRuntimeSettings: () => renderRuntimeSettings,
|
|
1062
|
-
renderPrivacyManifest: () => renderPrivacyManifest,
|
|
1063
|
-
renderOrientations: () => renderOrientations,
|
|
1064
|
-
renderEntitlements: () => renderEntitlements,
|
|
1065
|
-
renderDeviceFamilies: () => renderDeviceFamilies,
|
|
1066
|
-
renderBackgroundModes: () => renderBackgroundModes,
|
|
1067
|
-
renderAppearance: () => renderAppearance,
|
|
1068
|
-
productName: () => productName,
|
|
1069
|
-
pickSimulator: () => pickSimulator,
|
|
1070
|
-
orderSimulators: () => orderSimulators,
|
|
1071
|
-
open: () => open,
|
|
1072
|
-
installRuntime: () => installRuntime,
|
|
1073
|
-
init: () => init,
|
|
1074
|
-
build: () => build,
|
|
1075
|
-
bootSimulator: () => bootSimulator
|
|
1076
|
-
});
|
|
1077
|
-
import { cpSync as cpSync2, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync3 } from "fs";
|
|
1078
|
-
import { dirname, join as join2, resolve } from "path";
|
|
1079
|
-
function renderDeviceFamilies(config) {
|
|
1080
|
-
const values = new Set(((config.deviceFamilies?.length) ? config.deviceFamilies : ["iphone", "ipad"]).map((family) => family === "ipad" ? "2" : "1"));
|
|
1081
|
-
return [...values].join(",");
|
|
1082
|
-
}
|
|
1083
|
-
function xmlEscape(value) {
|
|
1084
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll(/\u0027/g, "'");
|
|
1085
|
-
}
|
|
1086
|
-
function plistString(key, value) {
|
|
1087
|
-
return ` <key>${key}</key>
|
|
1088
|
-
<string>${xmlEscape(value)}</string>`;
|
|
1089
|
-
}
|
|
1090
|
-
function resolveAppearance(config) {
|
|
1091
|
-
if (config.appearance)
|
|
1092
|
-
return config.appearance;
|
|
1093
|
-
return config.darkMode === false ? "light" : "dark";
|
|
1094
|
-
}
|
|
1095
|
-
function renderAppearance(config) {
|
|
1096
|
-
const appearance = resolveAppearance(config);
|
|
1097
|
-
if (appearance === "system")
|
|
1098
|
-
return { interfaceStyle: "Automatic", statusBarStyle: "UIStatusBarStyleDefault" };
|
|
1099
|
-
if (appearance === "light")
|
|
1100
|
-
return { interfaceStyle: "Light", statusBarStyle: "UIStatusBarStyleDarkContent" };
|
|
1101
|
-
return { interfaceStyle: "Dark", statusBarStyle: "UIStatusBarStyleLightContent" };
|
|
1102
|
-
}
|
|
1103
|
-
function renderUsageDescriptions(config) {
|
|
1104
|
-
const entries = [
|
|
1105
|
-
[config.enableSpeechRecognition, "NSSpeechRecognitionUsageDescription", `${config.appName} uses speech recognition for voice input.`],
|
|
1106
|
-
[config.enableSpeechRecognition || config.enableAudioRecording, "NSMicrophoneUsageDescription", `${config.appName} uses the microphone to record audio.`],
|
|
1107
|
-
[config.enableCamera || config.enableVideoRecording || config.enableQRScanner || config.enableAR, "NSCameraUsageDescription", `${config.appName} uses the camera for photos, video, scanning, and augmented reality.`],
|
|
1108
|
-
[config.enableCamera || config.enableVideoRecording, "NSPhotoLibraryUsageDescription", `${config.appName} lets you choose photos and videos from your library.`],
|
|
1109
|
-
[config.enableGeolocation || config.enableBackgroundLocation, "NSLocationWhenInUseUsageDescription", `${config.appName} uses your location while you record an activity.`],
|
|
1110
|
-
[config.enableBackgroundLocation, "NSLocationAlwaysAndWhenInUseUsageDescription", `${config.appName} continues recording your route when the screen is locked or the app is in the background.`],
|
|
1111
|
-
[config.enableContacts, "NSContactsUsageDescription", `${config.appName} accesses contacts only when you choose a contact feature.`],
|
|
1112
|
-
[config.enableCalendar, "NSCalendarsUsageDescription", `${config.appName} accesses your calendar only when you choose a calendar feature.`],
|
|
1113
|
-
[config.enableBluetooth, "NSBluetoothAlwaysUsageDescription", `${config.appName} uses Bluetooth to connect to nearby devices.`],
|
|
1114
|
-
[config.enableMotionSensors, "NSMotionUsageDescription", `${config.appName} uses motion data for activity features.`],
|
|
1115
|
-
[config.enableNFC, "NFCReaderUsageDescription", `${config.appName} reads NFC tags when you start a scan.`],
|
|
1116
|
-
[config.enableHealthKit, "NSHealthShareUsageDescription", `${config.appName} reads health data you choose to share.`],
|
|
1117
|
-
[config.enableHealthKit, "NSHealthUpdateUsageDescription", `${config.appName} writes health data only with your permission.`],
|
|
1118
|
-
[config.enableBiometric, "NSFaceIDUsageDescription", `${config.appName} uses Face ID to protect your account.`]
|
|
1119
|
-
];
|
|
1120
|
-
return entries.filter(([enabled2]) => enabled2).map(([, key, value]) => plistString(key, value)).join(`
|
|
1121
|
-
`);
|
|
1122
|
-
}
|
|
1123
|
-
function renderOrientations(config) {
|
|
1124
|
-
const names = {
|
|
1125
|
-
portrait: "UIInterfaceOrientationPortrait",
|
|
1126
|
-
"landscape-left": "UIInterfaceOrientationLandscapeLeft",
|
|
1127
|
-
"landscape-right": "UIInterfaceOrientationLandscapeRight",
|
|
1128
|
-
"portrait-upside-down": "UIInterfaceOrientationPortraitUpsideDown"
|
|
1129
|
-
};
|
|
1130
|
-
const values = config.orientations?.length ? config.orientations : ["portrait"];
|
|
1131
|
-
return values.map((value) => ` <string>${names[value]}</string>`).join(`
|
|
1132
|
-
`);
|
|
1133
|
-
}
|
|
1134
|
-
function renderUrlTypes(config) {
|
|
1135
|
-
const schemes = [...new Set(config.urlSchemes?.map((value) => value.trim()).filter(Boolean) ?? [])];
|
|
1136
|
-
if (!schemes.length)
|
|
1137
|
-
return "";
|
|
1138
|
-
return ` <key>CFBundleURLTypes</key>
|
|
1139
|
-
<array>
|
|
1140
|
-
<dict>
|
|
1141
|
-
<key>CFBundleURLSchemes</key>
|
|
1142
|
-
<array>
|
|
1143
|
-
${schemes.map((value) => ` <string>${xmlEscape(value)}</string>`).join(`
|
|
1144
|
-
`)}
|
|
1145
|
-
</array>
|
|
1146
|
-
</dict>
|
|
1147
|
-
</array>`;
|
|
1148
|
-
}
|
|
1149
|
-
function plistArray(values, indent = 2) {
|
|
1150
|
-
const padding = " ".repeat(indent);
|
|
1151
|
-
return values.map((value) => `${padding}<string>${xmlEscape(value)}</string>`).join(`
|
|
1152
|
-
`);
|
|
1153
|
-
}
|
|
1154
|
-
function renderBackgroundModes(config) {
|
|
1155
|
-
const modes = new Set;
|
|
1156
|
-
if (config.enableBackgroundLocation)
|
|
1157
|
-
modes.add("location");
|
|
1158
|
-
if (config.enableBackgroundTasks)
|
|
1159
|
-
modes.add("processing");
|
|
1160
|
-
if (config.enablePushNotifications)
|
|
1161
|
-
modes.add("remote-notification");
|
|
1162
|
-
if (!modes.size)
|
|
1163
|
-
return "";
|
|
1164
|
-
return ` <key>UIBackgroundModes</key>
|
|
1165
|
-
<array>
|
|
1166
|
-
${plistArray([...modes])}
|
|
1167
|
-
</array>`;
|
|
1168
|
-
}
|
|
1169
|
-
function renderEntitlements(config) {
|
|
1170
|
-
const entries = [];
|
|
1171
|
-
if (config.associatedDomains?.length) {
|
|
1172
|
-
entries.push(` <key>com.apple.developer.associated-domains</key>
|
|
1173
|
-
<array>
|
|
1174
|
-
${plistArray(config.associatedDomains)}
|
|
1175
|
-
</array>`);
|
|
1176
|
-
}
|
|
1177
|
-
if (config.appGroups?.length) {
|
|
1178
|
-
entries.push(` <key>com.apple.security.application-groups</key>
|
|
1179
|
-
<array>
|
|
1180
|
-
${plistArray(config.appGroups)}
|
|
1181
|
-
</array>`);
|
|
1182
|
-
}
|
|
1183
|
-
if (config.enableHealthKit) {
|
|
1184
|
-
entries.push(` <key>com.apple.developer.healthkit</key>
|
|
1185
|
-
<true/>`);
|
|
1186
|
-
}
|
|
1187
|
-
if (config.enablePushNotifications) {
|
|
1188
|
-
entries.push(` <key>aps-environment</key>
|
|
1189
|
-
<string>$(CRAFT_APNS_ENVIRONMENT)</string>`);
|
|
1190
|
-
}
|
|
1191
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1192
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1193
|
-
<plist version="1.0">
|
|
1194
|
-
<dict>
|
|
1195
|
-
${entries.join(`
|
|
1196
|
-
`)}
|
|
1197
|
-
</dict>
|
|
1198
|
-
</plist>
|
|
1199
|
-
`;
|
|
1200
|
-
}
|
|
1201
|
-
function renderWatchEntitlements(config) {
|
|
1202
|
-
const appGroups = config.appGroups?.length ? ` <key>com.apple.security.application-groups</key>
|
|
1203
|
-
<array>
|
|
1204
|
-
${plistArray(config.appGroups)}
|
|
1205
|
-
</array>
|
|
1206
|
-
` : "";
|
|
1207
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1208
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1209
|
-
<plist version="1.0">
|
|
1210
|
-
<dict>
|
|
1211
|
-
${appGroups}</dict>
|
|
1212
|
-
</plist>
|
|
1213
|
-
`;
|
|
1214
|
-
}
|
|
1215
|
-
function renderRuntimeSettings() {
|
|
1216
|
-
return [
|
|
1217
|
-
' LIBRARY_SEARCH_PATHS[sdk=iphoneos*]: "$(PROJECT_DIR)/Runtime/device"',
|
|
1218
|
-
' LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]: "$(PROJECT_DIR)/Runtime/simulator"',
|
|
1219
|
-
' OTHER_LDFLAGS: "-lcraft-ios -Wl,-u,_craft_ios_handle_action -Wl,-u,_craft_ios_set_webview ' + '-Wl,-u,_craft_ios_deliver_result -Wl,-u,_craft_ios_deliver_error"'
|
|
1220
|
-
].join(`
|
|
1221
|
-
`);
|
|
1222
|
-
}
|
|
1223
|
-
function renderPrivacyManifest(config) {
|
|
1224
|
-
const privacy = config.privacy ?? {};
|
|
1225
|
-
const collected = privacy.collectedDataTypes ?? [];
|
|
1226
|
-
const accessed = privacy.accessedApiTypes ?? [];
|
|
1227
|
-
const collectedXml = collected.map((item) => ` <dict>
|
|
1228
|
-
<key>NSPrivacyCollectedDataType</key>
|
|
1229
|
-
<string>${xmlEscape(item.type)}</string>
|
|
1230
|
-
<key>NSPrivacyCollectedDataTypeLinked</key>
|
|
1231
|
-
<${item.linked ? "true" : "false"}/>
|
|
1232
|
-
<key>NSPrivacyCollectedDataTypeTracking</key>
|
|
1233
|
-
<${item.tracking ? "true" : "false"}/>
|
|
1234
|
-
<key>NSPrivacyCollectedDataTypePurposes</key>
|
|
1235
|
-
<array>
|
|
1236
|
-
${plistArray(item.purposes, 4)}
|
|
1237
|
-
</array>
|
|
1238
|
-
</dict>`).join(`
|
|
1239
|
-
`);
|
|
1240
|
-
const accessedXml = accessed.map((item) => ` <dict>
|
|
1241
|
-
<key>NSPrivacyAccessedAPIType</key>
|
|
1242
|
-
<string>${xmlEscape(item.type)}</string>
|
|
1243
|
-
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
1244
|
-
<array>
|
|
1245
|
-
${plistArray(item.reasons, 4)}
|
|
1246
|
-
</array>
|
|
1247
|
-
</dict>`).join(`
|
|
1248
|
-
`);
|
|
1249
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
1250
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1251
|
-
<plist version="1.0">
|
|
1252
|
-
<dict>
|
|
1253
|
-
<key>NSPrivacyTracking</key>
|
|
1254
|
-
<${privacy.tracking ? "true" : "false"}/>
|
|
1255
|
-
<key>NSPrivacyTrackingDomains</key>
|
|
1256
|
-
<array>
|
|
1257
|
-
${plistArray(privacy.trackingDomains ?? [])}
|
|
1258
|
-
</array>
|
|
1259
|
-
<key>NSPrivacyCollectedDataTypes</key>
|
|
1260
|
-
<array>
|
|
1261
|
-
${collectedXml}
|
|
1262
|
-
</array>
|
|
1263
|
-
<key>NSPrivacyAccessedAPITypes</key>
|
|
1264
|
-
<array>
|
|
1265
|
-
${accessedXml}
|
|
1266
|
-
</array>
|
|
1267
|
-
</dict>
|
|
1268
|
-
</plist>
|
|
1269
|
-
`;
|
|
1270
|
-
}
|
|
1271
|
-
function renderAssetCatalog(output, config) {
|
|
1272
|
-
const catalog = join2(output, "Assets.xcassets");
|
|
1273
|
-
const appIcon = join2(catalog, "AppIcon.appiconset");
|
|
1274
|
-
const launchBackground = join2(catalog, "LaunchBackground.colorset");
|
|
1275
|
-
mkdirSync2(appIcon, { recursive: true });
|
|
1276
|
-
mkdirSync2(launchBackground, { recursive: true });
|
|
1277
|
-
writeFileSync3(join2(catalog, "Contents.json"), `${JSON.stringify({ info: { author: "xcode", version: 1 } }, null, 2)}
|
|
1278
|
-
`);
|
|
1279
|
-
const iconFilename = config.appIconPath ? "AppIcon-1024.png" : undefined;
|
|
1280
|
-
if (config.appIconPath) {
|
|
1281
|
-
if (!existsSync4(config.appIconPath))
|
|
1282
|
-
throw new Error(`App icon not found: ${config.appIconPath}`);
|
|
1283
|
-
cpSync2(config.appIconPath, join2(appIcon, iconFilename));
|
|
1284
|
-
}
|
|
1285
|
-
writeFileSync3(join2(appIcon, "Contents.json"), `${JSON.stringify({
|
|
1286
|
-
images: iconFilename ? [{
|
|
1287
|
-
filename: iconFilename,
|
|
1288
|
-
idiom: "universal",
|
|
1289
|
-
platform: "ios",
|
|
1290
|
-
size: "1024x1024"
|
|
1291
|
-
}] : [],
|
|
1292
|
-
info: { author: "xcode", version: 1 }
|
|
1293
|
-
}, null, 2)}
|
|
1294
|
-
`);
|
|
1295
|
-
const colorEntry = (hex) => {
|
|
1296
|
-
const color = hex?.replace(/^#/, "") || "000000";
|
|
1297
|
-
const normalized = color.length === 3 ? [...color].map((value) => `${value}${value}`).join("") : color.padEnd(6, "0").slice(0, 6);
|
|
1298
|
-
const components = [0, 2, 4].map((index) => (Number.parseInt(normalized.slice(index, index + 2), 16) / 255).toFixed(3));
|
|
1299
|
-
return {
|
|
1300
|
-
"color-space": "srgb",
|
|
1301
|
-
components: { alpha: "1.000", blue: components[2], green: components[1], red: components[0] }
|
|
1302
|
-
};
|
|
1303
|
-
};
|
|
1304
|
-
const colors2 = [{ color: colorEntry(config.backgroundColor), idiom: "universal" }];
|
|
1305
|
-
if (config.backgroundColorDark) {
|
|
1306
|
-
colors2.push({
|
|
1307
|
-
appearances: [{ appearance: "luminosity", value: "dark" }],
|
|
1308
|
-
color: colorEntry(config.backgroundColorDark),
|
|
1309
|
-
idiom: "universal"
|
|
1310
|
-
});
|
|
1311
|
-
}
|
|
1312
|
-
writeFileSync3(join2(launchBackground, "Contents.json"), `${JSON.stringify({
|
|
1313
|
-
colors: colors2,
|
|
1314
|
-
info: { author: "xcode", version: 1 }
|
|
1315
|
-
}, null, 2)}
|
|
1316
|
-
`);
|
|
1317
|
-
}
|
|
1318
|
-
function syncWebAssets(source, output) {
|
|
1319
|
-
const sourcePath = resolve(source);
|
|
1320
|
-
if (!existsSync4(sourcePath))
|
|
1321
|
-
throw new Error(`Web asset path not found: ${source}`);
|
|
1322
|
-
const distDir = join2(output, "dist");
|
|
1323
|
-
rmSync2(distDir, { recursive: true, force: true });
|
|
1324
|
-
mkdirSync2(distDir, { recursive: true });
|
|
1325
|
-
if (statSync(sourcePath).isDirectory()) {
|
|
1326
|
-
cpSync2(sourcePath, distDir, { recursive: true });
|
|
1327
|
-
} else {
|
|
1328
|
-
cpSync2(sourcePath, join2(distDir, "index.html"));
|
|
1329
|
-
}
|
|
1330
|
-
if (!existsSync4(join2(distDir, "index.html"))) {
|
|
1331
|
-
throw new Error(`Web asset directory must contain index.html: ${source}`);
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
function resolveRuntimeDir(override) {
|
|
1335
|
-
if (override === null)
|
|
1336
|
-
return null;
|
|
1337
|
-
const dir = override ?? process.env.CRAFT_IOS_RUNTIME;
|
|
1338
|
-
if (!dir)
|
|
1339
|
-
return null;
|
|
1340
|
-
if (!existsSync4(dir)) {
|
|
1341
|
-
const source = override === undefined ? "CRAFT_IOS_RUNTIME points at" : "runtimeDir is";
|
|
1342
|
-
throw new Error(`${source} ${dir}, which does not exist.`);
|
|
1343
|
-
}
|
|
1344
|
-
return dir;
|
|
1345
|
-
}
|
|
1346
|
-
async function installRuntime(output, runtimeDir) {
|
|
1347
|
-
const resolved = Object.entries(RUNTIME_ARCHIVES).map(([sdk, archives]) => {
|
|
1348
|
-
const present = archives.filter((a) => existsSync4(join2(runtimeDir, a)));
|
|
1349
|
-
if (present.length === 0) {
|
|
1350
|
-
throw new Error(`${runtimeDir} has none of ${archives.join(", ")}. ` + `Run \`zig build build-ios-all\` in packages/zig and point at its zig-out/lib.`);
|
|
1351
|
-
}
|
|
1352
|
-
return { sdk, archives, present };
|
|
1353
|
-
});
|
|
1354
|
-
const dest = join2(output, "Runtime");
|
|
1355
|
-
rmSync2(dest, { recursive: true, force: true });
|
|
1356
|
-
for (const { sdk, archives, present } of resolved) {
|
|
1357
|
-
const sdkDir = join2(dest, sdk);
|
|
1358
|
-
mkdirSync2(sdkDir, { recursive: true });
|
|
1359
|
-
const target = join2(sdkDir, "libcraft-ios.a");
|
|
1360
|
-
if (present.length === 1) {
|
|
1361
|
-
const missing = archives.filter((a) => !present.includes(a));
|
|
1362
|
-
if (missing.length > 0) {
|
|
1363
|
-
console.warn(` \u26A0 ${sdk}: only ${present[0]} was found; ${missing.join(", ")} is missing. ` + `The generated project will not link on the other architecture.`);
|
|
1364
|
-
}
|
|
1365
|
-
cpSync2(join2(runtimeDir, present[0]), target);
|
|
1366
|
-
} else {
|
|
1367
|
-
await $`lipo -create ${present.map((a) => join2(runtimeDir, a))} -output ${target}`.quiet();
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
return true;
|
|
1371
|
-
}
|
|
1372
|
-
async function refreshRuntime(output, override) {
|
|
1373
|
-
if (!existsSync4(join2(output, "Runtime")))
|
|
1374
|
-
return;
|
|
1375
|
-
const dir = resolveRuntimeDir(override);
|
|
1376
|
-
if (!dir) {
|
|
1377
|
-
console.log(" Keeping the Zig runtime installed at init (no runtime directory configured)");
|
|
1378
|
-
return;
|
|
1379
|
-
}
|
|
1380
|
-
await installRuntime(output, dir);
|
|
1381
|
-
console.log(" Refreshed the Zig runtime from", dir);
|
|
1382
|
-
}
|
|
1383
|
-
function productName(name) {
|
|
1384
|
-
const words = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
|
|
1385
|
-
const joined = words.map((word) => word[0].toUpperCase() + word.slice(1)).join("");
|
|
1386
|
-
if (!joined)
|
|
1387
|
-
return "Craft";
|
|
1388
|
-
return /^\d/.test(joined) ? `App${joined}` : joined;
|
|
1389
|
-
}
|
|
1390
|
-
async function init(options) {
|
|
1391
|
-
const { name: displayName, bundleId, teamId, output } = options;
|
|
1392
|
-
const name = productName(displayName);
|
|
1393
|
-
console.log(`
|
|
1394
|
-
\u26A1 Initializing Craft iOS project: ${displayName}`);
|
|
1395
|
-
console.log(` Output: ${output}
|
|
1396
|
-
`);
|
|
1397
|
-
const dirs = [output, join2(output, "Sources"), join2(output, "Shared"), join2(output, "dist")];
|
|
1398
|
-
if (options.config?.enableWatchApp)
|
|
1399
|
-
dirs.push(join2(output, "WatchApp"), join2(output, "WatchExtension"));
|
|
1400
|
-
for (const dir of dirs) {
|
|
1401
|
-
if (!existsSync4(dir)) {
|
|
1402
|
-
mkdirSync2(dir, { recursive: true });
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
|
-
const finalBundleId = bundleId || `com.craft.${name.toLowerCase()}`;
|
|
1406
|
-
const bundleIdPrefix = finalBundleId.split(".").slice(0, -1).join(".");
|
|
1407
|
-
const given = Object.fromEntries(Object.entries(options.config ?? {}).filter(([, value]) => value !== undefined));
|
|
1408
|
-
const config = {
|
|
1409
|
-
...DEFAULT_CONFIG,
|
|
1410
|
-
appName: displayName,
|
|
1411
|
-
bundleId: finalBundleId,
|
|
1412
|
-
teamId: teamId || "",
|
|
1413
|
-
...given
|
|
1414
|
-
};
|
|
1415
|
-
if (config.enableBackgroundLocation)
|
|
1416
|
-
config.enableGeolocation = true;
|
|
1417
|
-
writeFileSync3(join2(output, "craft.config.json"), JSON.stringify(config, null, 2));
|
|
1418
|
-
const swiftTemplate = readFileSync3(join2(TEMPLATES_DIR, "CraftApp.swift"), "utf-8");
|
|
1419
|
-
const swiftSource = swiftTemplate.replace(/CraftApp/g, `${name}App`).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
|
|
1420
|
-
writeFileSync3(join2(output, "Sources", `${name}App.swift`), swiftSource);
|
|
1421
|
-
const infoPlistTemplate = readFileSync3(join2(TEMPLATES_DIR, "Info.plist.template"), "utf-8");
|
|
1422
|
-
const infoPlist = infoPlistTemplate.replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{UI_STYLE\}\}/g, renderAppearance(config).interfaceStyle).replace(/\{\{STATUS_BAR_STYLE\}\}/g, renderAppearance(config).statusBarStyle).replace(/\{\{ORIENTATIONS\}\}/g, renderOrientations(config)).replace(/\{\{USAGE_DESCRIPTIONS\}\}/g, renderUsageDescriptions(config)).replace(/\{\{URL_TYPES\}\}/g, renderUrlTypes(config)).replace(/\{\{BACKGROUND_MODES\}\}/g, renderBackgroundModes(config)).replace(/\{\{LIVE_ACTIVITY_SUPPORT\}\}/g, config.enableLiveActivities ? ` <key>NSSupportsLiveActivities</key>
|
|
1423
|
-
<true/>
|
|
1424
|
-
<key>NSSupportsLiveActivitiesFrequentUpdates</key>
|
|
1425
|
-
<true/>` : "");
|
|
1426
|
-
writeFileSync3(join2(output, "Info.plist"), infoPlist);
|
|
1427
|
-
const projectYmlTemplate = readFileSync3(join2(TEMPLATES_DIR, "project.yml.template"), "utf-8");
|
|
1428
|
-
const nativeDependencies = [];
|
|
1429
|
-
if (config.enableLiveActivities)
|
|
1430
|
-
nativeDependencies.push(` - target: ${name}LiveActivity`);
|
|
1431
|
-
if (config.enableWatchApp)
|
|
1432
|
-
nativeDependencies.push(` - target: ${name}Watch
|
|
1433
|
-
embed: true`);
|
|
1434
|
-
const nativeTargets = [];
|
|
1435
|
-
if (config.enableLiveActivities) {
|
|
1436
|
-
nativeTargets.push(` ${name}LiveActivity:
|
|
1437
|
-
type: app-extension
|
|
1438
|
-
platform: iOS
|
|
1439
|
-
deploymentTarget: "16.2"
|
|
1440
|
-
sources:
|
|
1441
|
-
- WidgetExtension
|
|
1442
|
-
- Shared
|
|
1443
|
-
settings:
|
|
1444
|
-
INFOPLIST_FILE: WidgetExtension/Info.plist
|
|
1445
|
-
PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.liveactivity
|
|
1446
|
-
SWIFT_VERSION: "5.0"
|
|
1447
|
-
APPLICATION_EXTENSION_API_ONLY: YES
|
|
1448
|
-
SKIP_INSTALL: YES`);
|
|
1449
|
-
}
|
|
1450
|
-
if (config.enableWatchApp) {
|
|
1451
|
-
nativeTargets.push(` ${name}Watch:
|
|
1452
|
-
type: application.watchapp2
|
|
1453
|
-
platform: watchOS
|
|
1454
|
-
deploymentTarget: "${config.watchosVersion || "9.0"}"
|
|
1455
|
-
sources:
|
|
1456
|
-
- WatchApp
|
|
1457
|
-
settings:
|
|
1458
|
-
INFOPLIST_FILE: WatchApp/Info.plist
|
|
1459
|
-
CODE_SIGN_ENTITLEMENTS: WatchApp/Watch.entitlements
|
|
1460
|
-
PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.watchkitapp
|
|
1461
|
-
SWIFT_VERSION: "5.0"
|
|
1462
|
-
SKIP_INSTALL: YES
|
|
1463
|
-
dependencies:
|
|
1464
|
-
- target: ${name}WatchExtension
|
|
1465
|
-
${name}WatchExtension:
|
|
1466
|
-
type: watchkit2-extension
|
|
1467
|
-
platform: watchOS
|
|
1468
|
-
deploymentTarget: "${config.watchosVersion || "9.0"}"
|
|
1469
|
-
sources:
|
|
1470
|
-
- WatchExtension
|
|
1471
|
-
settings:
|
|
1472
|
-
INFOPLIST_FILE: WatchExtension/Info.plist
|
|
1473
|
-
CODE_SIGN_ENTITLEMENTS: WatchExtension/Watch.entitlements
|
|
1474
|
-
PRODUCT_BUNDLE_IDENTIFIER: ${finalBundleId}.watchkitapp.watchkitextension
|
|
1475
|
-
SWIFT_VERSION: "5.0"
|
|
1476
|
-
APPLICATION_EXTENSION_API_ONLY: YES
|
|
1477
|
-
SKIP_INSTALL: YES`);
|
|
1478
|
-
}
|
|
1479
|
-
const runtimeDir = resolveRuntimeDir(options.runtimeDir);
|
|
1480
|
-
const hasRuntime = runtimeDir ? await installRuntime(output, runtimeDir) : false;
|
|
1481
|
-
if (hasRuntime) {
|
|
1482
|
-
console.log(" Linked the Zig runtime from", runtimeDir);
|
|
1483
|
-
} else {
|
|
1484
|
-
rmSync2(join2(output, "Runtime"), { recursive: true, force: true });
|
|
1485
|
-
}
|
|
1486
|
-
const projectYml = projectYmlTemplate.replace(/\{\{CRAFT_RUNTIME_SETTINGS\}\}/g, hasRuntime ? renderRuntimeSettings() : "").replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId).replace(/\{\{BUNDLE_ID_PREFIX\}\}/g, bundleIdPrefix).replace(/\{\{VERSION\}\}/g, config.version || "1.0.0").replace(/\{\{BUILD_NUMBER\}\}/g, config.buildNumber || "1").replace(/\{\{IOS_VERSION\}\}/g, config.iosVersion || "15.0").replace(/\{\{DEVICE_FAMILIES\}\}/g, renderDeviceFamilies(config)).replace(/\{\{TEAM_ID\}\}/g, teamId || "").replace(/\{\{NATIVE_DEPENDENCIES\}\}/g, nativeDependencies.length ? ` dependencies:
|
|
1487
|
-
${nativeDependencies.join(`
|
|
1488
|
-
`)}` : "").replace(/\{\{NATIVE_TARGETS\}\}/g, nativeTargets.join(`
|
|
1489
|
-
`));
|
|
1490
|
-
writeFileSync3(join2(output, "project.yml"), projectYml);
|
|
1491
|
-
writeFileSync3(join2(output, "Craft.entitlements"), renderEntitlements(config));
|
|
1492
|
-
writeFileSync3(join2(output, "PrivacyInfo.xcprivacy"), renderPrivacyManifest(config));
|
|
1493
|
-
renderAssetCatalog(output, config);
|
|
1494
|
-
cpSync2(join2(TEMPLATES_DIR, "CraftActivityAttributes.swift"), join2(output, "Shared", "CraftActivityAttributes.swift"));
|
|
1495
|
-
if (config.enableLiveActivities) {
|
|
1496
|
-
mkdirSync2(join2(output, "WidgetExtension"), { recursive: true });
|
|
1497
|
-
const widgetSource = readFileSync3(join2(TEMPLATES_DIR, "CraftLiveActivityWidget.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
|
|
1498
|
-
writeFileSync3(join2(output, "WidgetExtension", `${name}LiveActivity.swift`), widgetSource);
|
|
1499
|
-
const widgetInfo = readFileSync3(join2(TEMPLATES_DIR, "WidgetExtension.Info.plist"), "utf8");
|
|
1500
|
-
writeFileSync3(join2(output, "WidgetExtension", "Info.plist"), widgetInfo);
|
|
1501
|
-
}
|
|
1502
|
-
if (config.enableWatchApp) {
|
|
1503
|
-
const watchSource = readFileSync3(join2(TEMPLATES_DIR, "CraftWatchApp.swift.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, name);
|
|
1504
|
-
writeFileSync3(join2(output, "WatchExtension", `${name}WatchApp.swift`), watchSource);
|
|
1505
|
-
const watchInfo = readFileSync3(join2(TEMPLATES_DIR, "WatchApp.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
|
|
1506
|
-
writeFileSync3(join2(output, "WatchApp", "Info.plist"), watchInfo);
|
|
1507
|
-
writeFileSync3(join2(output, "WatchApp", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
1508
|
-
const watchExtensionInfo = readFileSync3(join2(TEMPLATES_DIR, "WatchExtension.Info.plist.template"), "utf8").replace(/\{\{APP_NAME\}\}/g, xmlEscape(displayName)).replace(/\{\{BUNDLE_ID\}\}/g, finalBundleId);
|
|
1509
|
-
writeFileSync3(join2(output, "WatchExtension", "Info.plist"), watchExtensionInfo);
|
|
1510
|
-
writeFileSync3(join2(output, "WatchExtension", "Watch.entitlements"), renderWatchEntitlements(config));
|
|
1511
|
-
}
|
|
1512
|
-
const placeholderHtml = `<!DOCTYPE html>
|
|
1513
|
-
<html>
|
|
1514
|
-
<head>
|
|
1515
|
-
<meta charset="UTF-8">
|
|
1516
|
-
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
1517
|
-
<title>${xmlEscape(displayName)}</title>
|
|
1518
|
-
<style>
|
|
1519
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
1520
|
-
body {
|
|
1521
|
-
font-family: -apple-system, system-ui, sans-serif;
|
|
1522
|
-
background: ${config.backgroundColor};
|
|
1523
|
-
color: white;
|
|
1524
|
-
min-height: 100vh;
|
|
1525
|
-
display: flex;
|
|
1526
|
-
justify-content: center;
|
|
1527
|
-
align-items: center;
|
|
1528
|
-
padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
|
1529
|
-
}
|
|
1530
|
-
.container { text-align: center; padding: 2rem; }
|
|
1531
|
-
h1 { font-size: 2.5rem; margin-bottom: 1rem; }
|
|
1532
|
-
p { opacity: 0.7; }
|
|
1533
|
-
.ready { color: #4ade80; font-size: 0.9rem; margin-top: 2rem; }
|
|
1534
|
-
</style>
|
|
1535
|
-
</head>
|
|
1536
|
-
<body>
|
|
1537
|
-
<div class="container">
|
|
1538
|
-
<h1>\u26A1 ${xmlEscape(displayName)}</h1>
|
|
1539
|
-
<p>Built with Craft iOS</p>
|
|
1540
|
-
<p class="ready" id="status">Waiting for Craft bridge...</p>
|
|
1541
|
-
</div>
|
|
1542
|
-
<script>
|
|
1543
|
-
window.addEventListener('craftReady', (e) => {
|
|
1544
|
-
document.getElementById('status').textContent = \`\u2713 Craft bridge ready (platform: \${e.detail.platform})\`;
|
|
1545
|
-
console.log('Craft capabilities:', e.detail.capabilities);
|
|
1546
|
-
});
|
|
1547
|
-
</script>
|
|
1548
|
-
</body>
|
|
1549
|
-
</html>`;
|
|
1550
|
-
writeFileSync3(join2(output, "dist", "index.html"), placeholderHtml);
|
|
1551
|
-
console.log("\u2705 Project initialized");
|
|
1552
|
-
console.log("");
|
|
1553
|
-
console.log("Next steps:");
|
|
1554
|
-
console.log(` 1. cd ${output}`);
|
|
1555
|
-
console.log(" 2. Add your web content to dist/index.html");
|
|
1556
|
-
console.log(" 3. Run: craft ios build");
|
|
1557
|
-
console.log(" 4. Run: craft ios open");
|
|
1558
|
-
console.log("");
|
|
1559
|
-
}
|
|
1560
|
-
async function build(options) {
|
|
1561
|
-
const { htmlPath, devServer, output, generateProject = true } = options;
|
|
1562
|
-
console.log(`
|
|
1563
|
-
\uD83D\uDCE6 Building Craft iOS project...`);
|
|
1564
|
-
const configPath = join2(output, "craft.config.json");
|
|
1565
|
-
if (!existsSync4(configPath)) {
|
|
1566
|
-
throw new Error(`No craft.config.json found in ${output}. Run 'craft ios init' first.`);
|
|
1567
|
-
}
|
|
1568
|
-
const config = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
1569
|
-
if (devServer) {
|
|
1570
|
-
config.devServerURL = devServer;
|
|
1571
|
-
const origin = new URL(devServer).origin;
|
|
1572
|
-
config.trustedOrigins = [...new Set([...config.trustedOrigins ?? [], origin])];
|
|
1573
|
-
writeFileSync3(configPath, JSON.stringify(config, null, 2));
|
|
1574
|
-
console.log(` Dev server: ${devServer}`);
|
|
1575
|
-
}
|
|
1576
|
-
if (htmlPath) {
|
|
1577
|
-
syncWebAssets(htmlPath, output);
|
|
1578
|
-
console.log(` Synced: ${htmlPath} \u2192 dist/`);
|
|
1579
|
-
}
|
|
1580
|
-
await refreshRuntime(output, options.runtimeDir);
|
|
1581
|
-
if (!generateProject)
|
|
1582
|
-
return;
|
|
1583
|
-
try {
|
|
1584
|
-
const result = await $`which xcodegen`.quiet();
|
|
1585
|
-
if (result.exitCode === 0) {
|
|
1586
|
-
console.log(" Running xcodegen...");
|
|
1587
|
-
await $`cd ${output} && xcodegen generate`.quiet();
|
|
1588
|
-
console.log(`\u2705 Xcode project created: ${productName(config.appName)}.xcodeproj`);
|
|
1589
|
-
} else {
|
|
1590
|
-
throw new Error("xcodegen not found");
|
|
1591
|
-
}
|
|
1592
|
-
} catch (error) {
|
|
1593
|
-
throw new Error("Unable to generate the Xcode project. Install xcodegen with `brew install xcodegen`.", {
|
|
1594
|
-
cause: error
|
|
1595
|
-
});
|
|
1596
|
-
}
|
|
1597
|
-
console.log("");
|
|
1598
|
-
}
|
|
1599
|
-
async function open(options) {
|
|
1600
|
-
const { output } = options;
|
|
1601
|
-
const files = readdirSync2(output);
|
|
1602
|
-
const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
|
|
1603
|
-
if (!xcodeproj) {
|
|
1604
|
-
throw new Error(`No Xcode project found in ${output}. Run 'craft ios build' first.`);
|
|
1605
|
-
}
|
|
1606
|
-
const projectPath = join2(output, xcodeproj);
|
|
1607
|
-
console.log(`\uD83D\uDE80 Opening ${xcodeproj}...`);
|
|
1608
|
-
await $`open ${projectPath}`;
|
|
1609
|
-
}
|
|
1610
|
-
function orderSimulators(devices) {
|
|
1611
|
-
return devices.filter((device) => device.runtime.startsWith("iOS")).sort((left, right) => {
|
|
1612
|
-
const booted = Number(right.state === "Booted") - Number(left.state === "Booted");
|
|
1613
|
-
if (booted !== 0)
|
|
1614
|
-
return booted;
|
|
1615
|
-
const phone = Number(right.name.startsWith("iPhone")) - Number(left.name.startsWith("iPhone"));
|
|
1616
|
-
if (phone !== 0)
|
|
1617
|
-
return phone;
|
|
1618
|
-
return right.runtime.localeCompare(left.runtime);
|
|
1619
|
-
});
|
|
1620
|
-
}
|
|
1621
|
-
async function pickSimulator() {
|
|
1622
|
-
const listed = await $`xcrun simctl list devices available --json`.quiet().nothrow();
|
|
1623
|
-
if (listed.exitCode !== 0)
|
|
1624
|
-
return null;
|
|
1625
|
-
let parsed;
|
|
1626
|
-
try {
|
|
1627
|
-
parsed = JSON.parse(listed.stdout.toString());
|
|
1628
|
-
} catch {
|
|
1629
|
-
return null;
|
|
1630
|
-
}
|
|
1631
|
-
const devices = [];
|
|
1632
|
-
for (const [runtime, list] of Object.entries(parsed.devices ?? {})) {
|
|
1633
|
-
for (const device of list)
|
|
1634
|
-
devices.push({ ...device, runtime: runtime.split(".").pop() ?? runtime });
|
|
1635
|
-
}
|
|
1636
|
-
return orderSimulators(devices)[0] ?? null;
|
|
1637
|
-
}
|
|
1638
|
-
async function bootSimulator(device) {
|
|
1639
|
-
if (device.state !== "Booted")
|
|
1640
|
-
await $`xcrun simctl boot ${device.udid}`.quiet().nothrow();
|
|
1641
|
-
await $`xcrun simctl bootstatus ${device.udid}`.quiet().nothrow();
|
|
1642
|
-
}
|
|
1643
|
-
async function showSimulator() {
|
|
1644
|
-
const selected = await $`xcode-select -p`.quiet().nothrow();
|
|
1645
|
-
const developer = selected.exitCode === 0 ? selected.stdout.toString().trim() : "";
|
|
1646
|
-
if (developer) {
|
|
1647
|
-
const app = join2(developer, "Applications", "Simulator.app");
|
|
1648
|
-
if (existsSync4(app)) {
|
|
1649
|
-
await $`open ${app}`.quiet().nothrow();
|
|
1650
|
-
return;
|
|
1651
|
-
}
|
|
1652
|
-
}
|
|
1653
|
-
await $`open -a Simulator`.quiet().nothrow();
|
|
1654
|
-
}
|
|
1655
|
-
async function run(options) {
|
|
1656
|
-
const { simulator, output } = options;
|
|
1657
|
-
await build({ output });
|
|
1658
|
-
const files = readdirSync2(output);
|
|
1659
|
-
const xcodeproj = files.find((f) => f.endsWith(".xcodeproj"));
|
|
1660
|
-
if (!xcodeproj) {
|
|
1661
|
-
throw new Error(`No Xcode project found in ${output}`);
|
|
1662
|
-
}
|
|
1663
|
-
const projectPath = join2(output, xcodeproj);
|
|
1664
|
-
const appName = xcodeproj.replace(".xcodeproj", "");
|
|
1665
|
-
const configPath = join2(output, "craft.config.json");
|
|
1666
|
-
if (!existsSync4(configPath))
|
|
1667
|
-
throw new Error(`No craft.config.json found in ${output}. Run 'craft-ios init' first.`);
|
|
1668
|
-
const config = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
1669
|
-
if (simulator) {
|
|
1670
|
-
console.log("\uD83D\uDCF1 Building and running on simulator...");
|
|
1671
|
-
const device = await pickSimulator();
|
|
1672
|
-
if (!device) {
|
|
1673
|
-
throw new Error("No iOS simulator is available. Install a runtime with " + "`xcodebuild -downloadPlatform iOS`, then try again.");
|
|
1674
|
-
}
|
|
1675
|
-
console.log(` Device: ${device.name} (${device.runtime})`);
|
|
1676
|
-
try {
|
|
1677
|
-
await bootSimulator(device);
|
|
1678
|
-
const derivedData = join2(output, "build");
|
|
1679
|
-
const product = join2(derivedData, "Build", "Products", "Debug-iphonesimulator", `${appName}.app`);
|
|
1680
|
-
await $`xcodebuild -project ${projectPath} -scheme ${appName} -configuration Debug -destination ${`id=${device.udid}`} -derivedDataPath ${derivedData} build`;
|
|
1681
|
-
if (!existsSync4(product)) {
|
|
1682
|
-
throw new Error(`xcodebuild reported success but ${product} does not exist. ` + "The scheme may build a different product name than the project.");
|
|
1683
|
-
}
|
|
1684
|
-
await $`xcrun simctl install ${device.udid} ${product}`;
|
|
1685
|
-
await showSimulator();
|
|
1686
|
-
await $`xcrun simctl launch ${device.udid} ${config.bundleId}`;
|
|
1687
|
-
console.log(`\u2705 ${config.appName} is running on ${device.name}`);
|
|
1688
|
-
} catch (error) {
|
|
1689
|
-
throw new Error(`iOS simulator run failed for ${projectPath}`, { cause: error });
|
|
1690
|
-
}
|
|
1691
|
-
} else {
|
|
1692
|
-
console.log("\uD83D\uDCF1 Opening Xcode for device deployment...");
|
|
1693
|
-
await $`open ${projectPath}`;
|
|
1694
|
-
console.log("");
|
|
1695
|
-
console.log("In Xcode:");
|
|
1696
|
-
console.log(" 1. Select your Team in Signing & Capabilities");
|
|
1697
|
-
console.log(" 2. Connect your iPhone");
|
|
1698
|
-
console.log(" 3. Select your device");
|
|
1699
|
-
console.log(" 4. Click Run (\u25B6\uFE0F)");
|
|
1700
|
-
}
|
|
1701
|
-
}
|
|
1702
|
-
var $, TEMPLATES_DIR, DEFAULT_CONFIG, RUNTIME_ARCHIVES;
|
|
1703
|
-
var init_src = __esm(() => {
|
|
1704
|
-
({ $ } = globalThis.Bun);
|
|
1705
|
-
TEMPLATES_DIR = join2(dirname(import.meta.dir), "templates");
|
|
1706
|
-
DEFAULT_CONFIG = {
|
|
1707
|
-
version: "1.0.0",
|
|
1708
|
-
buildNumber: "1",
|
|
1709
|
-
darkMode: true,
|
|
1710
|
-
backgroundColor: "#0b1712",
|
|
1711
|
-
enableSpeechRecognition: false,
|
|
1712
|
-
enableHaptics: false,
|
|
1713
|
-
enableShare: false,
|
|
1714
|
-
enableCamera: false,
|
|
1715
|
-
enableBiometric: false,
|
|
1716
|
-
enablePushNotifications: false,
|
|
1717
|
-
enableSecureStorage: false,
|
|
1718
|
-
enableGeolocation: false,
|
|
1719
|
-
enableClipboard: false,
|
|
1720
|
-
enableContacts: false,
|
|
1721
|
-
enableCalendar: false,
|
|
1722
|
-
enableLocalNotifications: false,
|
|
1723
|
-
enableInAppPurchase: false,
|
|
1724
|
-
enableKeepAwake: false,
|
|
1725
|
-
enableOrientationLock: false,
|
|
1726
|
-
enableDeepLinks: false,
|
|
1727
|
-
enableQRScanner: false,
|
|
1728
|
-
enableFilePicker: false,
|
|
1729
|
-
enableFileDownload: false,
|
|
1730
|
-
enableSocialAuth: false,
|
|
1731
|
-
enableAudioRecording: false,
|
|
1732
|
-
enableVideoRecording: false,
|
|
1733
|
-
enableMotionSensors: false,
|
|
1734
|
-
enableLocalDatabase: false,
|
|
1735
|
-
enableBluetooth: false,
|
|
1736
|
-
enableNFC: false,
|
|
1737
|
-
enableHealthKit: false,
|
|
1738
|
-
enableLiveActivities: false,
|
|
1739
|
-
enableWatchApp: false,
|
|
1740
|
-
enableBackgroundLocation: false,
|
|
1741
|
-
enableBackgroundTasks: false,
|
|
1742
|
-
enableScreenCapture: false,
|
|
1743
|
-
enablePDFViewer: false,
|
|
1744
|
-
enableAR: false,
|
|
1745
|
-
enableMLKit: false,
|
|
1746
|
-
iosVersion: "16.0",
|
|
1747
|
-
watchosVersion: "9.0",
|
|
1748
|
-
teamId: "",
|
|
1749
|
-
trustedOrigins: [],
|
|
1750
|
-
associatedDomains: [],
|
|
1751
|
-
appGroups: [],
|
|
1752
|
-
orientations: ["portrait"],
|
|
1753
|
-
deviceFamilies: ["iphone", "ipad"]
|
|
1754
|
-
};
|
|
1755
|
-
RUNTIME_ARCHIVES = {
|
|
1756
|
-
device: ["libcraft-ios.a"],
|
|
1757
|
-
simulator: ["libcraft-ios-simulator-arm64.a", "libcraft-ios-simulator-x64.a"]
|
|
1758
|
-
};
|
|
1759
|
-
});
|
|
1760
|
-
|
|
1761
|
-
// dist/android/src/index.js
|
|
1762
|
-
var exports_src2 = {};
|
|
1763
|
-
__export(exports_src2, {
|
|
1764
|
-
syncAndroidWebAssets: () => syncAndroidWebAssets,
|
|
1765
|
-
run: () => run2,
|
|
1766
|
-
resolveRuntimeDir: () => resolveRuntimeDir2,
|
|
1767
|
-
renderAndroidPermissions: () => renderAndroidPermissions,
|
|
1768
|
-
renderAndroidDeepLinks: () => renderAndroidDeepLinks,
|
|
1769
|
-
open: () => open2,
|
|
1770
|
-
installRuntime: () => installRuntime2,
|
|
1771
|
-
init: () => init2,
|
|
1772
|
-
build: () => build2,
|
|
1773
|
-
ANDROID_MIN_SDK: () => ANDROID_MIN_SDK
|
|
1774
|
-
});
|
|
1775
|
-
import { cpSync as cpSync3, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
1776
|
-
import { dirname as dirname2, extname, isAbsolute, join as join3, relative, resolve as resolve2, sep } from "path";
|
|
1777
|
-
function renderAndroidPromiseRuntime(indent = "") {
|
|
1778
|
-
return ANDROID_PROMISE_RUNTIME.trim().split(`
|
|
1779
|
-
`).map((line) => `${indent}${line}`).join(`
|
|
1780
|
-
`);
|
|
1781
|
-
}
|
|
1782
|
-
function escapeXml(value) {
|
|
1783
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
1784
|
-
}
|
|
1785
|
-
function escapeKotlinString(value) {
|
|
1786
|
-
return value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("$", "\\$").replaceAll("\r", "\\r").replaceAll(`
|
|
1787
|
-
`, "\\n");
|
|
1788
|
-
}
|
|
1789
|
-
function generatedPackageSegment(name) {
|
|
1790
|
-
const normalized = name.toLowerCase().replace(/[^a-z0-9_]/g, "");
|
|
1791
|
-
if (!normalized)
|
|
1792
|
-
return "app";
|
|
1793
|
-
return /^[a-z_]/.test(normalized) ? normalized : `app${normalized}`;
|
|
1794
|
-
}
|
|
1795
|
-
function generatedGradleProjectName(name) {
|
|
1796
|
-
const invalidCharacters = ["/", "\\", ":", "<", ">", '"', "?", "*", "|"];
|
|
1797
|
-
const normalized = invalidCharacters.reduce((value, character) => value.replaceAll(character, "-"), name).trim();
|
|
1798
|
-
return normalized || "craft-app";
|
|
1799
|
-
}
|
|
1800
|
-
function requireRegularFile(path2, label) {
|
|
1801
|
-
if (!existsSync5(path2))
|
|
1802
|
-
throw new Error(`${label} not found: ${path2}`);
|
|
1803
|
-
if (!statSync2(path2).isFile())
|
|
1804
|
-
throw new Error(`${label} must be a file: ${path2}`);
|
|
1805
|
-
}
|
|
1806
|
-
function containsPath(parent, candidate) {
|
|
1807
|
-
const relativePath = relative(parent, candidate);
|
|
1808
|
-
return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
|
|
1809
|
-
}
|
|
1810
|
-
function validateGoogleServicesFile(path2, packageName) {
|
|
1811
|
-
requireRegularFile(path2, "Google services file");
|
|
1812
|
-
let document;
|
|
1813
|
-
try {
|
|
1814
|
-
document = JSON.parse(readFileSync4(path2, "utf8"));
|
|
1815
|
-
} catch (error) {
|
|
1816
|
-
throw new Error(`Google services file must contain valid JSON: ${path2}`, { cause: error });
|
|
1817
|
-
}
|
|
1818
|
-
const matchingClient = document.client?.some((client) => {
|
|
1819
|
-
return client.client_info?.android_client_info?.package_name === packageName;
|
|
1820
|
-
});
|
|
1821
|
-
if (!matchingClient) {
|
|
1822
|
-
throw new Error(`Google services file has no client for Android package ${packageName}: ${path2}`);
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
function androidWebUrl(value, field) {
|
|
1826
|
-
let url;
|
|
1827
|
-
try {
|
|
1828
|
-
url = new URL(value);
|
|
1829
|
-
} catch {
|
|
1830
|
-
throw new Error(`Invalid ${field}: ${value}`);
|
|
1831
|
-
}
|
|
1832
|
-
const localDevelopment = url.protocol === "http:" && LOCAL_DEVELOPMENT_HOSTS.has(url.hostname);
|
|
1833
|
-
if (url.protocol !== "https:" && !localDevelopment || url.username || url.password) {
|
|
1834
|
-
throw new Error(`${field} must use HTTPS or local HTTP without credentials: ${value}`);
|
|
1835
|
-
}
|
|
1836
|
-
return url;
|
|
1837
|
-
}
|
|
1838
|
-
function normalizeAndroidNetworkConfig(config) {
|
|
1839
|
-
const schemes = config.urlSchemes?.map((value) => value.trim().toLowerCase()) ?? [];
|
|
1840
|
-
if (schemes.some((value) => !/^[a-z][a-z0-9+.-]*$/.test(value))) {
|
|
1841
|
-
throw new Error("Android deep-link schemes must be valid URI schemes");
|
|
1842
|
-
}
|
|
1843
|
-
config.urlSchemes = [...new Set(schemes)];
|
|
1844
|
-
if (config.enableDeepLinks && config.urlSchemes.length === 0) {
|
|
1845
|
-
throw new Error("Android deep links require at least one URL scheme");
|
|
1846
|
-
}
|
|
1847
|
-
const trustedOrigins = (config.trustedOrigins ?? []).map((value) => {
|
|
1848
|
-
return androidWebUrl(value, "Android trusted origin").origin;
|
|
1849
|
-
});
|
|
1850
|
-
if (config.devServerURL) {
|
|
1851
|
-
const devServer = androidWebUrl(config.devServerURL, "Android dev server URL");
|
|
1852
|
-
config.devServerURL = devServer.toString();
|
|
1853
|
-
trustedOrigins.push(devServer.origin);
|
|
1854
|
-
}
|
|
1855
|
-
config.trustedOrigins = [...new Set(trustedOrigins)];
|
|
1856
|
-
}
|
|
1857
|
-
function validateAndroidConfig(config) {
|
|
1858
|
-
if (!config.appName.trim())
|
|
1859
|
-
throw new Error("Android app name must not be empty");
|
|
1860
|
-
const packageSegments = config.packageName.split(".");
|
|
1861
|
-
if (packageSegments.length < 2 || packageSegments.some((segment) => !/^[A-Za-z_][A-Za-z0-9_]*$/.test(segment) || KOTLIN_KEYWORDS.has(segment))) {
|
|
1862
|
-
throw new Error(`Invalid Android package name: ${config.packageName}`);
|
|
1863
|
-
}
|
|
1864
|
-
if (!/^#(?:[\dA-F]{3,4}|[\dA-F]{6}|[\dA-F]{8})$/i.test(config.backgroundColor ?? "")) {
|
|
1865
|
-
throw new Error(`Invalid Android background color: ${config.backgroundColor}`);
|
|
1866
|
-
}
|
|
1867
|
-
for (const [name, value] of [
|
|
1868
|
-
["versionCode", config.versionCode],
|
|
1869
|
-
["minSdk", config.minSdk],
|
|
1870
|
-
["compileSdk", config.compileSdk],
|
|
1871
|
-
["targetSdk", config.targetSdk]
|
|
1872
|
-
]) {
|
|
1873
|
-
if (!Number.isInteger(value) || Number(value) < 1) {
|
|
1874
|
-
throw new Error(`Android ${name} must be a positive integer`);
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
|
-
if (Number(config.minSdk) < ANDROID_MIN_SDK) {
|
|
1878
|
-
throw new Error(`Android minSdk must be at least ${ANDROID_MIN_SDK} (Android 8.0) for the Craft runtime`);
|
|
1879
|
-
}
|
|
1880
|
-
if (Number(config.minSdk) > Number(config.targetSdk)) {
|
|
1881
|
-
throw new Error("Android minSdk must not exceed targetSdk");
|
|
1882
|
-
}
|
|
1883
|
-
if (Number(config.targetSdk) > Number(config.compileSdk)) {
|
|
1884
|
-
throw new Error("Android targetSdk must not exceed compileSdk");
|
|
1885
|
-
}
|
|
1886
|
-
}
|
|
1887
|
-
function writeAndroidConfig(output, config) {
|
|
1888
|
-
writeFileSync4(join3(output, "craft.config.json"), JSON.stringify(config, null, 2));
|
|
1889
|
-
const runtimeConfig = { ...config };
|
|
1890
|
-
delete runtimeConfig.appIconPath;
|
|
1891
|
-
delete runtimeConfig.googleServicesFile;
|
|
1892
|
-
writeFileSync4(join3(output, "app/src/main/assets/craft.config.json"), JSON.stringify(runtimeConfig, null, 2));
|
|
1893
|
-
}
|
|
1894
|
-
function syncAndroidWebAssets(source, output) {
|
|
1895
|
-
const sourcePath = resolve2(source);
|
|
1896
|
-
if (!existsSync5(sourcePath))
|
|
1897
|
-
throw new Error(`Web asset path not found: ${source}`);
|
|
1898
|
-
const sourceStat = statSync2(sourcePath);
|
|
1899
|
-
if (!sourceStat.isDirectory() && !sourceStat.isFile()) {
|
|
1900
|
-
throw new Error(`Web asset path must be a file or directory: ${source}`);
|
|
1901
|
-
}
|
|
1902
|
-
const assetsDir = resolve2(output, "app/src/main/assets");
|
|
1903
|
-
if (containsPath(sourcePath, assetsDir) || containsPath(assetsDir, sourcePath)) {
|
|
1904
|
-
throw new Error(`Web asset source must not overlap generated asset directory: ${source}`);
|
|
1905
|
-
}
|
|
1906
|
-
if (sourceStat.isDirectory()) {
|
|
1907
|
-
requireRegularFile(join3(sourcePath, "index.html"), "Web asset directory entry point");
|
|
1908
|
-
}
|
|
1909
|
-
const configPath = join3(assetsDir, "craft.config.json");
|
|
1910
|
-
const config = existsSync5(configPath) ? readFileSync4(configPath) : undefined;
|
|
1911
|
-
rmSync3(assetsDir, { recursive: true, force: true });
|
|
1912
|
-
mkdirSync3(assetsDir, { recursive: true });
|
|
1913
|
-
if (sourceStat.isDirectory())
|
|
1914
|
-
cpSync3(sourcePath, assetsDir, { recursive: true });
|
|
1915
|
-
else
|
|
1916
|
-
cpSync3(sourcePath, join3(assetsDir, "index.html"));
|
|
1917
|
-
if (!existsSync5(join3(assetsDir, "index.html")))
|
|
1918
|
-
throw new Error(`Web asset directory must contain index.html: ${source}`);
|
|
1919
|
-
if (config)
|
|
1920
|
-
writeFileSync4(configPath, config);
|
|
1921
|
-
}
|
|
1922
|
-
function renderAndroidPermissions(config) {
|
|
1923
|
-
const permissions = new Set(["android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE"]);
|
|
1924
|
-
if (config.enableSpeechRecognition)
|
|
1925
|
-
permissions.add("android.permission.RECORD_AUDIO");
|
|
1926
|
-
if (config.enableCamera)
|
|
1927
|
-
permissions.add("android.permission.CAMERA");
|
|
1928
|
-
if (config.enableHaptics)
|
|
1929
|
-
permissions.add("android.permission.VIBRATE");
|
|
1930
|
-
if (config.enableBiometric)
|
|
1931
|
-
permissions.add("android.permission.USE_BIOMETRIC");
|
|
1932
|
-
if (config.enableGeolocation || config.enableBackgroundLocation) {
|
|
1933
|
-
permissions.add("android.permission.ACCESS_FINE_LOCATION");
|
|
1934
|
-
permissions.add("android.permission.ACCESS_COARSE_LOCATION");
|
|
1935
|
-
}
|
|
1936
|
-
if (config.enableBackgroundLocation) {
|
|
1937
|
-
permissions.add("android.permission.ACCESS_BACKGROUND_LOCATION");
|
|
1938
|
-
permissions.add("android.permission.FOREGROUND_SERVICE");
|
|
1939
|
-
permissions.add("android.permission.FOREGROUND_SERVICE_LOCATION");
|
|
1940
|
-
}
|
|
1941
|
-
if (config.enablePushNotifications)
|
|
1942
|
-
permissions.add("android.permission.POST_NOTIFICATIONS");
|
|
1943
|
-
if (config.enableKeepAwake)
|
|
1944
|
-
permissions.add("android.permission.WAKE_LOCK");
|
|
1945
|
-
if (config.enableHealthConnect) {
|
|
1946
|
-
permissions.add("android.permission.health.READ_STEPS");
|
|
1947
|
-
permissions.add("android.permission.health.READ_HEART_RATE");
|
|
1948
|
-
permissions.add("android.permission.health.READ_ACTIVE_CALORIES_BURNED");
|
|
1949
|
-
permissions.add("android.permission.health.READ_DISTANCE");
|
|
1950
|
-
permissions.add("android.permission.health.READ_EXERCISE");
|
|
1951
|
-
permissions.add("android.permission.health.WRITE_EXERCISE");
|
|
1952
|
-
permissions.add("android.permission.health.WRITE_ACTIVE_CALORIES_BURNED");
|
|
1953
|
-
permissions.add("android.permission.health.WRITE_DISTANCE");
|
|
1954
|
-
}
|
|
1955
|
-
return [...permissions].map((permission) => ` <uses-permission android:name="${permission}" />`).join(`
|
|
1956
|
-
`);
|
|
1957
|
-
}
|
|
1958
|
-
function renderAndroidDeepLinks(config) {
|
|
1959
|
-
if (!config.enableDeepLinks)
|
|
1960
|
-
return "";
|
|
1961
|
-
const schemes = [...new Set(config.urlSchemes?.map((value) => value.trim()).filter((value) => /^[a-z][a-z0-9+.-]*$/i.test(value)) ?? [])];
|
|
1962
|
-
return schemes.map((scheme) => ` <intent-filter>
|
|
1963
|
-
<action android:name="android.intent.action.VIEW" />
|
|
1964
|
-
<category android:name="android.intent.category.DEFAULT" />
|
|
1965
|
-
<category android:name="android.intent.category.BROWSABLE" />
|
|
1966
|
-
<data android:scheme="${scheme}" />
|
|
1967
|
-
</intent-filter>`).join(`
|
|
1968
|
-
`);
|
|
1969
|
-
}
|
|
1970
|
-
function resolveRuntimeDir2(override) {
|
|
1971
|
-
if (override === null)
|
|
1972
|
-
return null;
|
|
1973
|
-
const dir = override ?? process.env.CRAFT_ANDROID_RUNTIME;
|
|
1974
|
-
if (!dir)
|
|
1975
|
-
return null;
|
|
1976
|
-
if (!existsSync5(dir)) {
|
|
1977
|
-
const source = override === undefined ? "CRAFT_ANDROID_RUNTIME points at" : "runtimeDir is";
|
|
1978
|
-
throw new Error(`${source} ${dir}, which does not exist.`);
|
|
1979
|
-
}
|
|
1980
|
-
return dir;
|
|
1981
|
-
}
|
|
1982
|
-
function installRuntime2(output, runtimeDir) {
|
|
1983
|
-
const found = RUNTIME_ABIS.map((abi) => ({ abi, source: join3(runtimeDir, abi, "libcraft.so") })).filter((entry) => existsSync5(entry.source));
|
|
1984
|
-
if (found.length === 0) {
|
|
1985
|
-
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.");
|
|
1986
|
-
}
|
|
1987
|
-
if (found.length < RUNTIME_ABIS.length) {
|
|
1988
|
-
const missing = RUNTIME_ABIS.filter((abi) => !found.some((entry) => entry.abi === abi));
|
|
1989
|
-
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.");
|
|
1990
|
-
}
|
|
1991
|
-
const dest = join3(output, "app/src/main/jniLibs");
|
|
1992
|
-
rmSync3(dest, { force: true, recursive: true });
|
|
1993
|
-
for (const { abi, source } of found) {
|
|
1994
|
-
const abiDir = join3(dest, abi);
|
|
1995
|
-
mkdirSync3(abiDir, { recursive: true });
|
|
1996
|
-
cpSync3(source, join3(abiDir, "libcraft.so"));
|
|
1997
|
-
}
|
|
1998
|
-
return true;
|
|
1999
|
-
}
|
|
2000
|
-
async function init2(options) {
|
|
2001
|
-
const { name, packageName, output } = options;
|
|
2002
|
-
console.log(`
|
|
2003
|
-
\u26A1 Initializing Craft Android project: ${name}`);
|
|
2004
|
-
console.log(` Output: ${output}
|
|
2005
|
-
`);
|
|
2006
|
-
const finalPackageName = packageName || `com.craft.${generatedPackageSegment(name)}`;
|
|
2007
|
-
const packagePath = finalPackageName.replace(/\./g, "/");
|
|
2008
|
-
const given = Object.fromEntries(Object.entries(options.config ?? {}).filter(([, value]) => value !== undefined));
|
|
2009
|
-
const config = {
|
|
2010
|
-
...DEFAULT_CONFIG2,
|
|
2011
|
-
...given,
|
|
2012
|
-
appName: name,
|
|
2013
|
-
packageName: finalPackageName
|
|
2014
|
-
};
|
|
2015
|
-
if (config.enableBackgroundLocation)
|
|
2016
|
-
config.enableGeolocation = true;
|
|
2017
|
-
if (config.enableHealthConnect) {
|
|
2018
|
-
config.compileSdk = Math.max(config.compileSdk ?? 36, 36);
|
|
2019
|
-
}
|
|
2020
|
-
normalizeAndroidNetworkConfig(config);
|
|
2021
|
-
validateAndroidConfig(config);
|
|
2022
|
-
if (config.enablePushNotifications && !config.googleServicesFile) {
|
|
2023
|
-
throw new Error("Android push notifications require a googleServicesFile");
|
|
2024
|
-
}
|
|
2025
|
-
if (config.googleServicesFile)
|
|
2026
|
-
validateGoogleServicesFile(config.googleServicesFile, finalPackageName);
|
|
2027
|
-
if (config.appIconPath)
|
|
2028
|
-
requireRegularFile(config.appIconPath, "App icon");
|
|
2029
|
-
const appIconExtension = config.appIconPath ? extname(config.appIconPath).toLowerCase() : undefined;
|
|
2030
|
-
if (config.appIconPath && ![".gif", ".jpg", ".png", ".webp"].includes(appIconExtension ?? "")) {
|
|
2031
|
-
throw new Error(`Unsupported Android app icon format: ${appIconExtension || "(none)"}`);
|
|
2032
|
-
}
|
|
2033
|
-
const dirs = [
|
|
2034
|
-
output,
|
|
2035
|
-
join3(output, "app/src/main/java", packagePath),
|
|
2036
|
-
join3(output, "app/src/main/res/layout"),
|
|
2037
|
-
join3(output, "app/src/main/res/values"),
|
|
2038
|
-
join3(output, "app/src/main/res/drawable"),
|
|
2039
|
-
join3(output, "app/src/main/assets"),
|
|
2040
|
-
join3(output, "gradle/wrapper")
|
|
2041
|
-
];
|
|
2042
|
-
if (existsSync5(output) && !statSync2(output).isDirectory()) {
|
|
2043
|
-
throw new Error(`Android project output must be a directory: ${output}`);
|
|
2044
|
-
}
|
|
2045
|
-
for (const dir of dirs) {
|
|
2046
|
-
if (!existsSync5(dir)) {
|
|
2047
|
-
mkdirSync3(dir, { recursive: true });
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
writeAndroidConfig(output, config);
|
|
2051
|
-
const hasGoogleServices = Boolean(config.googleServicesFile);
|
|
2052
|
-
if (config.googleServicesFile) {
|
|
2053
|
-
cpSync3(config.googleServicesFile, join3(output, "app/google-services.json"));
|
|
2054
|
-
}
|
|
2055
|
-
const mainActivityTemplate = readFileSync4(join3(TEMPLATES_DIR2, "MainActivity.kt.template"), "utf-8");
|
|
2056
|
-
const mainActivity = mainActivityTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName).replace(/\{\{APP_NAME\}\}/g, name);
|
|
2057
|
-
writeFileSync4(join3(output, "app/src/main/java", packagePath, "MainActivity.kt"), mainActivity);
|
|
2058
|
-
const craftBridgeTemplate = readFileSync4(join3(TEMPLATES_DIR2, "CraftBridge.kt.template"), "utf-8");
|
|
2059
|
-
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 {
|
|
2060
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
|
|
2061
|
-
&& ContextCompat.checkSelfPermission(activity, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
|
2062
|
-
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 4204)
|
|
2063
|
-
}
|
|
2064
|
-
try {
|
|
2065
|
-
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
|
|
2066
|
-
val token = if (task.isSuccessful) task.result else null
|
|
2067
|
-
val callback = if (token.isNullOrBlank()) "window._craftPushReject" else "window._craftPushResolve"
|
|
2068
|
-
val payload = JSONObject.quote(token ?: task.exception?.message ?: "Firebase Cloud Messaging is not configured")
|
|
2069
|
-
evaluatePromiseJavascript("$callback && $callback($payload)")
|
|
2070
|
-
}
|
|
2071
|
-
} catch (error: Exception) {
|
|
2072
|
-
val payload = JSONObject.quote(error.message ?: "Firebase Cloud Messaging is not configured")
|
|
2073
|
-
evaluatePromiseJavascript("window._craftPushReject && window._craftPushReject($payload)")
|
|
2074
|
-
}
|
|
2075
|
-
}` : `evaluatePromiseJavascript(
|
|
2076
|
-
"window._craftPushReject && window._craftPushReject('Push notifications are disabled')"
|
|
2077
|
-
)`);
|
|
2078
|
-
writeFileSync4(join3(output, "app/src/main/java", packagePath, "CraftBridge.kt"), craftBridge);
|
|
2079
|
-
const craftNative = readFileSync4(join3(TEMPLATES_DIR2, "CraftNative.kt.template"), "utf-8");
|
|
2080
|
-
const nativeDir = join3(output, "app/src/main/java/com/craft/runtime");
|
|
2081
|
-
mkdirSync3(nativeDir, { recursive: true });
|
|
2082
|
-
writeFileSync4(join3(nativeDir, "CraftNative.kt"), craftNative);
|
|
2083
|
-
const serviceTemplate = readFileSync4(join3(TEMPLATES_DIR2, "LocationRecordingService.kt.template"), "utf-8");
|
|
2084
|
-
writeFileSync4(join3(nativeDir, "LocationRecordingService.kt"), serviceTemplate);
|
|
2085
|
-
const healthTemplate = readFileSync4(join3(TEMPLATES_DIR2, config.enableHealthConnect ? "CraftHealthConnect.kt.template" : "CraftHealthConnectStub.kt.template"), "utf-8");
|
|
2086
|
-
writeFileSync4(join3(output, "app/src/main/java", packagePath, "CraftHealthConnect.kt"), healthTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
2087
|
-
const manifestTemplate = readFileSync4(join3(TEMPLATES_DIR2, "AndroidManifest.xml.template"), "utf-8");
|
|
2088
|
-
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>
|
|
2089
|
-
<package android:name="com.google.android.apps.healthdata" />
|
|
2090
|
-
</queries>` : "").replace(/\{\{HEALTH_CONNECT_RATIONALE\}\}/g, config.enableHealthConnect ? ` <intent-filter>
|
|
2091
|
-
<action android:name="androidx.health.ACTION_SHOW_PERMISSIONS_RATIONALE" />
|
|
2092
|
-
</intent-filter>
|
|
2093
|
-
<intent-filter>
|
|
2094
|
-
<action android:name="android.intent.action.VIEW_PERMISSION_USAGE" />
|
|
2095
|
-
<category android:name="android.intent.category.HEALTH_PERMISSIONS" />
|
|
2096
|
-
</intent-filter>` : "");
|
|
2097
|
-
writeFileSync4(join3(output, "app/src/main/AndroidManifest.xml"), manifest);
|
|
2098
|
-
const projectGradleTemplate = readFileSync4(join3(TEMPLATES_DIR2, "build.gradle.kts.project.template"), "utf-8");
|
|
2099
|
-
writeFileSync4(join3(output, "build.gradle.kts"), projectGradleTemplate.replace(/\{\{GOOGLE_SERVICES_PLUGIN\}\}/g, hasGoogleServices ? ' id("com.google.gms.google-services") version "4.4.2" apply false' : ""));
|
|
2100
|
-
const appGradleTemplate = readFileSync4(join3(TEMPLATES_DIR2, "build.gradle.kts.app.template"), "utf-8");
|
|
2101
|
-
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(/\{\{HEALTH_CONNECT_DEPENDENCIES\}\}/g, config.enableHealthConnect ? ` implementation("androidx.health.connect:connect-client:1.1.0")
|
|
2102
|
-
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1")` : "");
|
|
2103
|
-
writeFileSync4(join3(output, "app/build.gradle.kts"), appGradle);
|
|
2104
|
-
const proguardTemplate = readFileSync4(join3(TEMPLATES_DIR2, "proguard-rules.pro.template"), "utf-8");
|
|
2105
|
-
writeFileSync4(join3(output, "app/proguard-rules.pro"), proguardTemplate.replace(/\{\{PACKAGE_NAME\}\}/g, finalPackageName));
|
|
2106
|
-
const settingsTemplate = readFileSync4(join3(TEMPLATES_DIR2, "settings.gradle.kts.template"), "utf-8");
|
|
2107
|
-
const settings2 = settingsTemplate.replace(/\{\{APP_NAME\}\}/g, escapeKotlinString(generatedGradleProjectName(name)));
|
|
2108
|
-
writeFileSync4(join3(output, "settings.gradle.kts"), settings2);
|
|
2109
|
-
const gradleProperties = `org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
|
|
2110
|
-
android.useAndroidX=true
|
|
2111
|
-
kotlin.code.style=official
|
|
2112
|
-
android.nonTransitiveRClass=true
|
|
2113
|
-
`;
|
|
2114
|
-
writeFileSync4(join3(output, "gradle.properties"), gradleProperties);
|
|
2115
|
-
writeFileSync4(join3(output, "local.properties"), `# SDK location will be set by Android Studio
|
|
2116
|
-
`);
|
|
2117
|
-
const gradleWrapperProps = `distributionBase=GRADLE_USER_HOME
|
|
2118
|
-
distributionPath=wrapper/dists
|
|
2119
|
-
distributionUrl=https\\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
|
|
2120
|
-
networkTimeout=10000
|
|
2121
|
-
validateDistributionUrl=true
|
|
2122
|
-
zipStoreBase=GRADLE_USER_HOME
|
|
2123
|
-
zipStorePath=wrapper/dists
|
|
2124
|
-
`;
|
|
2125
|
-
writeFileSync4(join3(output, "gradle/wrapper/gradle-wrapper.properties"), gradleWrapperProps);
|
|
2126
|
-
const stringsXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
2127
|
-
<resources>
|
|
2128
|
-
<string name="app_name">${escapeXml(name)}</string>
|
|
2129
|
-
</resources>
|
|
2130
|
-
`;
|
|
2131
|
-
writeFileSync4(join3(output, "app/src/main/res/values/strings.xml"), stringsXml);
|
|
2132
|
-
const colorsXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
2133
|
-
<resources>
|
|
2134
|
-
<color name="primary">#1a1a2e</color>
|
|
2135
|
-
<color name="primary_dark">#16162b</color>
|
|
2136
|
-
<color name="accent">#4ade80</color>
|
|
2137
|
-
<color name="background">${config.backgroundColor}</color>
|
|
2138
|
-
</resources>
|
|
2139
|
-
`;
|
|
2140
|
-
writeFileSync4(join3(output, "app/src/main/res/values/colors.xml"), colorsXml);
|
|
2141
|
-
const appIconXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
2142
|
-
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2143
|
-
android:width="108dp"
|
|
2144
|
-
android:height="108dp"
|
|
2145
|
-
android:viewportWidth="108"
|
|
2146
|
-
android:viewportHeight="108">
|
|
2147
|
-
<path android:fillColor="${config.backgroundColor}" android:pathData="M0,0h108v108h-108z" />
|
|
2148
|
-
<path android:fillColor="#10B981" android:pathData="M18,78L42,42l12,18l12,-12l24,30z" />
|
|
2149
|
-
</vector>
|
|
2150
|
-
`;
|
|
2151
|
-
if (config.appIconPath) {
|
|
2152
|
-
cpSync3(config.appIconPath, join3(output, `app/src/main/res/drawable/craft_app_icon${appIconExtension}`));
|
|
2153
|
-
} else {
|
|
2154
|
-
writeFileSync4(join3(output, "app/src/main/res/drawable/craft_app_icon.xml"), appIconXml);
|
|
2155
|
-
}
|
|
2156
|
-
const themesXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
2157
|
-
<resources>
|
|
2158
|
-
<style name="Theme.CraftApp" parent="Theme.Material3.DayNight.NoActionBar">
|
|
2159
|
-
<item name="android:statusBarColor">@color/primary_dark</item>
|
|
2160
|
-
<item name="android:navigationBarColor">@color/primary</item>
|
|
2161
|
-
<item name="android:windowBackground">@color/background</item>
|
|
2162
|
-
</style>
|
|
2163
|
-
</resources>
|
|
2164
|
-
`;
|
|
2165
|
-
writeFileSync4(join3(output, "app/src/main/res/values/themes.xml"), themesXml);
|
|
2166
|
-
const activityMainXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
2167
|
-
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
|
2168
|
-
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2169
|
-
android:layout_width="match_parent"
|
|
2170
|
-
android:layout_height="match_parent"
|
|
2171
|
-
android:fitsSystemWindows="true">
|
|
2172
|
-
|
|
2173
|
-
<WebView
|
|
2174
|
-
android:id="@+id/webview"
|
|
2175
|
-
android:layout_width="match_parent"
|
|
2176
|
-
android:layout_height="match_parent" />
|
|
2177
|
-
|
|
2178
|
-
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
2179
|
-
`;
|
|
2180
|
-
writeFileSync4(join3(output, "app/src/main/res/layout/activity_main.xml"), activityMainXml);
|
|
2181
|
-
const placeholderHtml = `<!DOCTYPE html>
|
|
2182
|
-
<html>
|
|
2183
|
-
<head>
|
|
2184
|
-
<meta charset="UTF-8">
|
|
2185
|
-
<meta name="viewport" content="viewport-fit=cover, width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
2186
|
-
<title>${escapeXml(name)}</title>
|
|
2187
|
-
<style>
|
|
2188
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
2189
|
-
body {
|
|
2190
|
-
font-family: 'Roboto', sans-serif;
|
|
2191
|
-
background: ${config.backgroundColor};
|
|
2192
|
-
color: white;
|
|
2193
|
-
min-height: 100vh;
|
|
2194
|
-
display: flex;
|
|
2195
|
-
justify-content: center;
|
|
2196
|
-
align-items: center;
|
|
2197
|
-
}
|
|
2198
|
-
.container { text-align: center; padding: 2rem; }
|
|
2199
|
-
h1 { font-size: 2.5rem; margin-bottom: 1rem; }
|
|
2200
|
-
p { opacity: 0.7; }
|
|
2201
|
-
.ready { color: #4ade80; font-size: 0.9rem; margin-top: 2rem; }
|
|
2202
|
-
</style>
|
|
2203
|
-
</head>
|
|
2204
|
-
<body>
|
|
2205
|
-
<div class="container">
|
|
2206
|
-
<h1>\u26A1 ${escapeXml(name)}</h1>
|
|
2207
|
-
<p>Built with Craft Android</p>
|
|
2208
|
-
<p class="ready" id="status">Waiting for Craft bridge...</p>
|
|
2209
|
-
</div>
|
|
2210
|
-
<script>
|
|
2211
|
-
window.addEventListener('craftReady', (e) => {
|
|
2212
|
-
document.getElementById('status').textContent = \`\u2713 Craft bridge ready (platform: \${e.detail.platform})\`;
|
|
2213
|
-
console.log('Craft capabilities:', e.detail.capabilities);
|
|
2214
|
-
});
|
|
2215
|
-
</script>
|
|
2216
|
-
</body>
|
|
2217
|
-
</html>`;
|
|
2218
|
-
writeFileSync4(join3(output, "app/src/main/assets/index.html"), placeholderHtml);
|
|
2219
|
-
const runtimeDir = resolveRuntimeDir2(options.runtimeDir);
|
|
2220
|
-
if (runtimeDir) {
|
|
2221
|
-
installRuntime2(output, runtimeDir);
|
|
2222
|
-
console.log(" Installed the Zig runtime from", runtimeDir);
|
|
2223
|
-
}
|
|
2224
|
-
console.log("\u2705 Project initialized");
|
|
2225
|
-
console.log("");
|
|
2226
|
-
console.log("Next steps:");
|
|
2227
|
-
console.log(` 1. cd ${output}`);
|
|
2228
|
-
console.log(" 2. Add your web content to app/src/main/assets/index.html");
|
|
2229
|
-
console.log(" 3. Run: craft android build");
|
|
2230
|
-
console.log(" 4. Run: craft android open");
|
|
2231
|
-
console.log("");
|
|
2232
|
-
}
|
|
2233
|
-
async function build2(options) {
|
|
2234
|
-
const { htmlPath, devServer, output, release, compile = true } = options;
|
|
2235
|
-
console.log(`
|
|
2236
|
-
\uD83D\uDCE6 Building Craft Android project...`);
|
|
2237
|
-
const configPath = join3(output, "craft.config.json");
|
|
2238
|
-
if (!existsSync5(configPath)) {
|
|
2239
|
-
throw new Error(`No craft.config.json found in ${output}. Run 'craft android init' first.`);
|
|
2240
|
-
}
|
|
2241
|
-
const config = JSON.parse(readFileSync4(configPath, "utf-8"));
|
|
2242
|
-
if (existsSync5(join3(output, "app/src/main/jniLibs"))) {
|
|
2243
|
-
const runtimeDir = resolveRuntimeDir2(options.runtimeDir);
|
|
2244
|
-
if (runtimeDir) {
|
|
2245
|
-
installRuntime2(output, runtimeDir);
|
|
2246
|
-
console.log(" Refreshed the Zig runtime from", runtimeDir);
|
|
2247
|
-
} else {
|
|
2248
|
-
console.log(" Keeping the Zig runtime installed at init (no runtime directory configured)");
|
|
2249
|
-
}
|
|
2250
|
-
}
|
|
2251
|
-
if (devServer) {
|
|
2252
|
-
const url = androidWebUrl(devServer, "Android dev server URL");
|
|
2253
|
-
config.devServerURL = url.toString();
|
|
2254
|
-
config.trustedOrigins = [...new Set([
|
|
2255
|
-
...(config.trustedOrigins ?? []).map((value) => androidWebUrl(value, "Android trusted origin").origin),
|
|
2256
|
-
url.origin
|
|
2257
|
-
])];
|
|
2258
|
-
writeAndroidConfig(output, config);
|
|
2259
|
-
console.log(` Dev server: ${devServer}`);
|
|
2260
|
-
}
|
|
2261
|
-
if (htmlPath) {
|
|
2262
|
-
syncAndroidWebAssets(htmlPath, output);
|
|
2263
|
-
config.hasBundledFallback = Boolean(devServer);
|
|
2264
|
-
writeAndroidConfig(output, config);
|
|
2265
|
-
console.log(` Synced: ${htmlPath} \u2192 assets/`);
|
|
2266
|
-
}
|
|
2267
|
-
if (!compile)
|
|
2268
|
-
return;
|
|
2269
|
-
const gradlewPath = join3(output, "gradlew");
|
|
2270
|
-
if (!existsSync5(gradlewPath)) {
|
|
2271
|
-
console.log(" Generating Gradle wrapper...");
|
|
2272
|
-
try {
|
|
2273
|
-
await $2`cd ${output} && gradle wrapper`.quiet();
|
|
2274
|
-
} catch (error) {
|
|
2275
|
-
throw new Error("Gradle is unavailable. Install Android Studio or Gradle before building Android.", { cause: error });
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
const buildType = release ? "assembleRelease" : "assembleDebug";
|
|
2279
|
-
console.log(` Building ${release ? "release" : "debug"} APK...`);
|
|
2280
|
-
try {
|
|
2281
|
-
await $2`cd ${output} && ./gradlew ${buildType}`;
|
|
2282
|
-
const apkPath = release ? "app/build/outputs/apk/release/app-release.apk" : "app/build/outputs/apk/debug/app-debug.apk";
|
|
2283
|
-
console.log(`\u2705 APK built: ${apkPath}`);
|
|
2284
|
-
} catch (error) {
|
|
2285
|
-
console.error("Build failed. Open in Android Studio for details.");
|
|
2286
|
-
throw error;
|
|
2287
|
-
}
|
|
2288
|
-
console.log("");
|
|
2289
|
-
}
|
|
2290
|
-
async function open2(options) {
|
|
2291
|
-
const { output } = options;
|
|
2292
|
-
if (!existsSync5(output)) {
|
|
2293
|
-
throw new Error(`No Android project found in ${output}. Run 'craft android init' first.`);
|
|
2294
|
-
}
|
|
2295
|
-
console.log(`\uD83D\uDE80 Opening Android project in Android Studio...`);
|
|
2296
|
-
try {
|
|
2297
|
-
await $2`open -a "Android Studio" ${output}`.quiet();
|
|
2298
|
-
} catch {
|
|
2299
|
-
try {
|
|
2300
|
-
await $2`studio ${output}`.quiet();
|
|
2301
|
-
} catch {
|
|
2302
|
-
console.log("\u26A0\uFE0F Android Studio not found.");
|
|
2303
|
-
console.log(" Please open the project manually in Android Studio:");
|
|
2304
|
-
console.log(` ${output}`);
|
|
2305
|
-
}
|
|
2306
|
-
}
|
|
2307
|
-
}
|
|
2308
|
-
async function run2(options) {
|
|
2309
|
-
const { device, output } = options;
|
|
2310
|
-
await build2({ output });
|
|
2311
|
-
console.log("\uD83D\uDCF1 Installing and running on device...");
|
|
2312
|
-
try {
|
|
2313
|
-
const apkPath = join3(output, "app/build/outputs/apk/debug/app-debug.apk");
|
|
2314
|
-
if (device) {
|
|
2315
|
-
await $2`adb -s ${device} install -r ${apkPath}`;
|
|
2316
|
-
} else {
|
|
2317
|
-
await $2`adb install -r ${apkPath}`;
|
|
2318
|
-
}
|
|
2319
|
-
const config = JSON.parse(readFileSync4(join3(output, "craft.config.json"), "utf-8"));
|
|
2320
|
-
const launchCmd = `${config.packageName}/.MainActivity`;
|
|
2321
|
-
if (device) {
|
|
2322
|
-
await $2`adb -s ${device} shell am start -n ${launchCmd}`;
|
|
2323
|
-
} else {
|
|
2324
|
-
await $2`adb shell am start -n ${launchCmd}`;
|
|
2325
|
-
}
|
|
2326
|
-
console.log("\u2705 App launched");
|
|
2327
|
-
} catch (error) {
|
|
2328
|
-
console.error("Failed to install/run. Make sure:");
|
|
2329
|
-
console.error(" 1. ADB is installed and in PATH");
|
|
2330
|
-
console.error(" 2. A device/emulator is connected (run: adb devices)");
|
|
2331
|
-
throw error;
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
var $2, ANDROID_PROMISE_RUNTIME = `
|
|
2335
|
-
if (window.__craftRejectPendingPromises) {
|
|
2336
|
-
window.__craftRejectPendingPromises('Android bridge reinitialized');
|
|
2337
|
-
}
|
|
2338
|
-
if (window.__craftRejectPermissionRequests) {
|
|
2339
|
-
window.__craftRejectPermissionRequests('Android bridge reinitialized');
|
|
2340
|
-
}
|
|
2341
|
-
window.__craftPromiseRuntimeClosed = false;
|
|
2342
|
-
window.__craftPendingPromises = Object.create(null);
|
|
2343
|
-
window.__craftPromise = function(channel, resolveName, rejectName, invoke, timeoutMs, timeoutError) {
|
|
2344
|
-
if (window.__craftPromiseRuntimeClosed) {
|
|
2345
|
-
return Promise.reject(new Error('Android bridge is closed'));
|
|
2346
|
-
}
|
|
2347
|
-
if (window.__craftPendingPromises[channel]) {
|
|
2348
|
-
return Promise.reject(new Error('A '.concat(channel, ' request is already in progress')));
|
|
2349
|
-
}
|
|
2350
|
-
|
|
2351
|
-
return new Promise(function(resolve, reject) {
|
|
2352
|
-
var entry = {settled: false, timer: null, settle: null};
|
|
2353
|
-
var resolveCallback;
|
|
2354
|
-
var rejectCallback;
|
|
2355
|
-
|
|
2356
|
-
entry.settle = function(succeeded, value) {
|
|
2357
|
-
if (entry.settled || window.__craftPendingPromises[channel] !== entry) return;
|
|
2358
|
-
entry.settled = true;
|
|
2359
|
-
if (entry.timer !== null) clearTimeout(entry.timer);
|
|
2360
|
-
if (window[resolveName] === resolveCallback) window[resolveName] = null;
|
|
2361
|
-
if (window[rejectName] === rejectCallback) window[rejectName] = null;
|
|
2362
|
-
delete window.__craftPendingPromises[channel];
|
|
2363
|
-
if (succeeded) resolve(value);
|
|
2364
|
-
else reject(value);
|
|
2365
|
-
};
|
|
2366
|
-
|
|
2367
|
-
resolveCallback = function(value) {
|
|
2368
|
-
entry.settle(true, value);
|
|
2369
|
-
};
|
|
2370
|
-
rejectCallback = function(error) {
|
|
2371
|
-
entry.settle(false, error);
|
|
2372
|
-
};
|
|
2373
|
-
window[resolveName] = resolveCallback;
|
|
2374
|
-
window[rejectName] = rejectCallback;
|
|
2375
|
-
window.__craftPendingPromises[channel] = entry;
|
|
2376
|
-
|
|
2377
|
-
if (timeoutMs > 0) {
|
|
2378
|
-
entry.timer = setTimeout(function() {
|
|
2379
|
-
entry.settle(false, timeoutError || new Error(channel.concat(' request timed out')));
|
|
2380
|
-
}, timeoutMs);
|
|
2381
|
-
}
|
|
2382
|
-
|
|
2383
|
-
try {
|
|
2384
|
-
invoke();
|
|
2385
|
-
}
|
|
2386
|
-
catch (error) {
|
|
2387
|
-
entry.settle(false, error);
|
|
2388
|
-
}
|
|
2389
|
-
});
|
|
2390
|
-
};
|
|
2391
|
-
|
|
2392
|
-
window.__craftRejectPendingPromises = function(message) {
|
|
2393
|
-
window.__craftPromiseRuntimeClosed = true;
|
|
2394
|
-
Object.keys(window.__craftPendingPromises).forEach(function(channel) {
|
|
2395
|
-
var entry = window.__craftPendingPromises[channel];
|
|
2396
|
-
if (entry) entry.settle(false, new Error(message || 'Android bridge closed'));
|
|
2397
|
-
});
|
|
2398
|
-
};
|
|
2399
|
-
`, TEMPLATES_DIR2, LOCAL_DEVELOPMENT_HOSTS, KOTLIN_KEYWORDS, ANDROID_MIN_SDK = 26, DEFAULT_CONFIG2, RUNTIME_ABIS;
|
|
2400
|
-
var init_src2 = __esm(() => {
|
|
2401
|
-
({ $: $2 } = globalThis.Bun);
|
|
2402
|
-
TEMPLATES_DIR2 = join3(dirname2(import.meta.dir), "templates");
|
|
2403
|
-
LOCAL_DEVELOPMENT_HOSTS = new Set(["localhost", "127.0.0.1", "10.0.2.2"]);
|
|
2404
|
-
KOTLIN_KEYWORDS = new Set([
|
|
2405
|
-
"as",
|
|
2406
|
-
"break",
|
|
2407
|
-
"class",
|
|
2408
|
-
"continue",
|
|
2409
|
-
"do",
|
|
2410
|
-
"else",
|
|
2411
|
-
"false",
|
|
2412
|
-
"for",
|
|
2413
|
-
"fun",
|
|
2414
|
-
"if",
|
|
2415
|
-
"in",
|
|
2416
|
-
"interface",
|
|
2417
|
-
"is",
|
|
2418
|
-
"null",
|
|
2419
|
-
"object",
|
|
2420
|
-
"package",
|
|
2421
|
-
"return",
|
|
2422
|
-
"super",
|
|
2423
|
-
"this",
|
|
2424
|
-
"throw",
|
|
2425
|
-
"true",
|
|
2426
|
-
"try",
|
|
2427
|
-
"typealias",
|
|
2428
|
-
"typeof",
|
|
2429
|
-
"val",
|
|
2430
|
-
"var",
|
|
2431
|
-
"when",
|
|
2432
|
-
"while"
|
|
2433
|
-
]);
|
|
2434
|
-
DEFAULT_CONFIG2 = {
|
|
2435
|
-
version: "1.0.0",
|
|
2436
|
-
versionCode: 1,
|
|
2437
|
-
darkMode: true,
|
|
2438
|
-
backgroundColor: "#0b1712",
|
|
2439
|
-
enableSpeechRecognition: false,
|
|
2440
|
-
enableHaptics: false,
|
|
2441
|
-
enableShare: false,
|
|
2442
|
-
enableCamera: false,
|
|
2443
|
-
enableBiometric: false,
|
|
2444
|
-
enablePushNotifications: false,
|
|
2445
|
-
enableSecureStorage: false,
|
|
2446
|
-
enableGeolocation: false,
|
|
2447
|
-
enableBackgroundLocation: false,
|
|
2448
|
-
enableHealthConnect: false,
|
|
2449
|
-
enableKeepAwake: false,
|
|
2450
|
-
enableDeepLinks: false,
|
|
2451
|
-
urlSchemes: [],
|
|
2452
|
-
trustedOrigins: [],
|
|
2453
|
-
minSdk: ANDROID_MIN_SDK,
|
|
2454
|
-
compileSdk: 36,
|
|
2455
|
-
targetSdk: 35
|
|
2456
|
-
};
|
|
2457
|
-
RUNTIME_ABIS = ["arm64-v8a", "x86_64"];
|
|
2458
|
-
});
|
|
2459
|
-
|
|
2460
1142
|
// ../../node_modules/@stacksjs/clapp/dist/index.js
|
|
2461
1143
|
import { existsSync } from "fs";
|
|
2462
1144
|
import fs from "fs/promises";
|
|
@@ -4541,7 +3223,7 @@ var prefix = getPrefix();
|
|
|
4541
3223
|
|
|
4542
3224
|
// bin/cli.ts
|
|
4543
3225
|
import { spawn as spawn2 } from "child_process";
|
|
4544
|
-
import { existsSync as
|
|
3226
|
+
import { existsSync as existsSync4 } from "fs";
|
|
4545
3227
|
import process6 from "process";
|
|
4546
3228
|
|
|
4547
3229
|
// src/binary-resolver.ts
|
|
@@ -4621,7 +3303,7 @@ function pinCraftNativeDependency(packagePath, craftVersion) {
|
|
|
4621
3303
|
`);
|
|
4622
3304
|
}
|
|
4623
3305
|
// package.json
|
|
4624
|
-
var version = "0.0.
|
|
3306
|
+
var version = "0.0.107";
|
|
4625
3307
|
|
|
4626
3308
|
// bin/cli.ts
|
|
4627
3309
|
var spawnedFrom = process6.env[CRAFT_CLI_SPAWN_MARKER];
|
|
@@ -4630,16 +3312,18 @@ if (spawnedFrom) {
|
|
|
4630
3312
|
process6.exit(1);
|
|
4631
3313
|
}
|
|
4632
3314
|
var cli = new CLI("craft");
|
|
3315
|
+
var iosModuleURL = new URL("../dist/ios/src/index.js", import.meta.url).href;
|
|
3316
|
+
var androidModuleURL = new URL("../dist/android/src/index.js", import.meta.url).href;
|
|
4633
3317
|
async function runCraftBinary(args) {
|
|
4634
3318
|
const craftPath = resolveCraftBinary();
|
|
4635
|
-
return new Promise((
|
|
3319
|
+
return new Promise((resolve, reject) => {
|
|
4636
3320
|
const proc = spawn2(craftPath, args, {
|
|
4637
3321
|
stdio: "inherit",
|
|
4638
3322
|
env: { ...process6.env, [CRAFT_CLI_SPAWN_MARKER]: craftPath }
|
|
4639
3323
|
});
|
|
4640
3324
|
proc.on("exit", (code) => {
|
|
4641
3325
|
if (code === 0 || code === null)
|
|
4642
|
-
|
|
3326
|
+
resolve();
|
|
4643
3327
|
else
|
|
4644
3328
|
reject(new Error(`Craft exited with code ${code}`));
|
|
4645
3329
|
});
|
|
@@ -4733,7 +3417,7 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
|
|
|
4733
3417
|
console.log(`Platforms: ${platforms.join(", ")}`);
|
|
4734
3418
|
console.log(`Mode: ${options?.release ? "Release" : "Debug"}
|
|
4735
3419
|
`);
|
|
4736
|
-
const { existsSync:
|
|
3420
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
4737
3421
|
const { spawn: spawn3 } = await import("child_process");
|
|
4738
3422
|
for (const platform of platforms) {
|
|
4739
3423
|
console.log(`\uD83D\uDCE6 Building for ${platform}...`);
|
|
@@ -4744,9 +3428,9 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
|
|
|
4744
3428
|
continue;
|
|
4745
3429
|
}
|
|
4746
3430
|
const iosDir = "./ios";
|
|
4747
|
-
if (
|
|
3431
|
+
if (existsSync5(iosDir)) {
|
|
4748
3432
|
const buildType = options?.release ? "Release" : "Debug";
|
|
4749
|
-
await new Promise((
|
|
3433
|
+
await new Promise((resolve, reject) => {
|
|
4750
3434
|
const proc = spawn3("xcodebuild", [
|
|
4751
3435
|
"-project",
|
|
4752
3436
|
`${iosDir}/App.xcodeproj`,
|
|
@@ -4758,7 +3442,7 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
|
|
|
4758
3442
|
"iphoneos",
|
|
4759
3443
|
"build"
|
|
4760
3444
|
], { stdio: "inherit" });
|
|
4761
|
-
proc.on("exit", (code) => code === 0 ?
|
|
3445
|
+
proc.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Exit ${code}`)));
|
|
4762
3446
|
proc.on("error", reject);
|
|
4763
3447
|
});
|
|
4764
3448
|
console.log(` \u2705 iOS ${buildType} build complete`);
|
|
@@ -4767,14 +3451,14 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
|
|
|
4767
3451
|
}
|
|
4768
3452
|
} else if (platform === "android") {
|
|
4769
3453
|
const androidDir = "./android";
|
|
4770
|
-
if (
|
|
3454
|
+
if (existsSync5(androidDir)) {
|
|
4771
3455
|
const task = options?.release ? "assembleRelease" : "assembleDebug";
|
|
4772
|
-
await new Promise((
|
|
3456
|
+
await new Promise((resolve, reject) => {
|
|
4773
3457
|
const proc = spawn3("./gradlew", [task], {
|
|
4774
3458
|
cwd: androidDir,
|
|
4775
3459
|
stdio: "inherit"
|
|
4776
3460
|
});
|
|
4777
|
-
proc.on("exit", (code) => code === 0 ?
|
|
3461
|
+
proc.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Exit ${code}`)));
|
|
4778
3462
|
proc.on("error", reject);
|
|
4779
3463
|
});
|
|
4780
3464
|
console.log(` \u2705 Android ${options?.release ? "Release" : "Debug"} build complete`);
|
|
@@ -4782,9 +3466,9 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
|
|
|
4782
3466
|
console.log(` \u26A0\uFE0F No Android project found. Run: craft android init`);
|
|
4783
3467
|
}
|
|
4784
3468
|
} else if (platform === "macos" || platform === "windows" || platform === "linux") {
|
|
4785
|
-
const zigDir =
|
|
3469
|
+
const zigDir = existsSync5("./packages/zig") ? "./packages/zig" : ".";
|
|
4786
3470
|
const optimizeFlag = options?.release ? "-Doptimize=ReleaseSafe" : "";
|
|
4787
|
-
await new Promise((
|
|
3471
|
+
await new Promise((resolve, reject) => {
|
|
4788
3472
|
const args = ["build"];
|
|
4789
3473
|
if (optimizeFlag)
|
|
4790
3474
|
args.push(optimizeFlag);
|
|
@@ -4792,7 +3476,7 @@ cli.command("build", "Build the Craft application").option("--release", "Build i
|
|
|
4792
3476
|
cwd: zigDir,
|
|
4793
3477
|
stdio: "inherit"
|
|
4794
3478
|
});
|
|
4795
|
-
proc.on("exit", (code) => code === 0 ?
|
|
3479
|
+
proc.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`Exit ${code}`)));
|
|
4796
3480
|
proc.on("error", reject);
|
|
4797
3481
|
});
|
|
4798
3482
|
console.log(` \u2705 ${platform} build complete`);
|
|
@@ -4831,7 +3515,7 @@ cli.command("package", "Create installers for your Craft application").option("-
|
|
|
4831
3515
|
const { packageApp: packageApp2 } = packageModule;
|
|
4832
3516
|
let config;
|
|
4833
3517
|
if (options?.config) {
|
|
4834
|
-
if (!
|
|
3518
|
+
if (!existsSync4(options.config)) {
|
|
4835
3519
|
console.error(`\u274C Config file not found: ${options.config}`);
|
|
4836
3520
|
process6.exit(1);
|
|
4837
3521
|
}
|
|
@@ -4917,20 +3601,22 @@ cli.command("package", "Create installers for your Craft application").option("-
|
|
|
4917
3601
|
process6.exit(1);
|
|
4918
3602
|
}
|
|
4919
3603
|
});
|
|
4920
|
-
cli.command("ios init <name>", "Initialize a new iOS project").option("--bundle-id <id>", "Bundle identifier (e.g., com.example.app)").option("--team-id <id>", "Apple Developer Team ID").option("-o, --output <dir>", "Output directory", { default: "./ios" }).example("craft ios init MyApp").example("craft ios init MyApp --bundle-id com.example.myapp").action(async (name, options) => {
|
|
4921
|
-
const iosModule = await
|
|
3604
|
+
cli.command("ios init <name>", "Initialize a new iOS project").option("--bundle-id <id>", "Bundle identifier (e.g., com.example.app)").option("--team-id <id>", "Apple Developer Team ID").option("--renderer <kind>", "iOS renderer: web (default) or native").option("-o, --output <dir>", "Output directory", { default: "./ios" }).example("craft ios init MyApp").example("craft ios init MyApp --bundle-id com.example.myapp").action(async (name, options) => {
|
|
3605
|
+
const iosModule = await import(iosModuleURL);
|
|
4922
3606
|
await iosModule.init({
|
|
4923
3607
|
name,
|
|
4924
3608
|
bundleId: options?.bundleId,
|
|
4925
3609
|
teamId: options?.teamId,
|
|
4926
|
-
output: options?.output || "./ios"
|
|
3610
|
+
output: options?.output || "./ios",
|
|
3611
|
+
config: options?.renderer ? { renderer: options.renderer } : undefined
|
|
4927
3612
|
});
|
|
4928
3613
|
});
|
|
4929
|
-
cli.command("ios build", "Build iOS project").option("--html-path <path>", "Path to HTML file").option("-d, --dev-server <url>", "Development server URL").option("-o, --output <dir>", "iOS project directory", { default: "./ios" }).option("-w, --watch", "Watch for file changes and rebuild").example("craft ios build").example("craft ios build --html-path ./dist/index.html").example("craft ios build --dev-server http://localhost:3456").example("craft ios build --watch").action(async (options) => {
|
|
4930
|
-
const iosModule = await
|
|
3614
|
+
cli.command("ios build", "Build iOS project").option("--html-path <path>", "Path to HTML file").option("--native-bundle <path>", "JavaScript bundle compiled by stx-native").option("-d, --dev-server <url>", "Development server URL").option("-o, --output <dir>", "iOS project directory", { default: "./ios" }).option("-w, --watch", "Watch for file changes and rebuild").example("craft ios build").example("craft ios build --html-path ./dist/index.html").example("craft ios build --native-bundle ./screen.js").example("craft ios build --dev-server http://localhost:3456").example("craft ios build --watch").action(async (options) => {
|
|
3615
|
+
const iosModule = await import(iosModuleURL);
|
|
4931
3616
|
const doBuild = async () => {
|
|
4932
3617
|
await iosModule.build({
|
|
4933
3618
|
htmlPath: options?.htmlPath,
|
|
3619
|
+
nativeBundlePath: options?.nativeBundle,
|
|
4934
3620
|
devServer: options?.devServer,
|
|
4935
3621
|
output: options?.output || "./ios"
|
|
4936
3622
|
});
|
|
@@ -4941,8 +3627,8 @@ cli.command("ios build", "Build iOS project").option("--html-path <path>", "Path
|
|
|
4941
3627
|
\uD83D\uDC40 Watching for changes...
|
|
4942
3628
|
`);
|
|
4943
3629
|
const { watch } = await import("fs");
|
|
4944
|
-
const { dirname
|
|
4945
|
-
const watchPath = options?.htmlPath ?
|
|
3630
|
+
const { dirname } = await import("path");
|
|
3631
|
+
const watchPath = options?.htmlPath ? dirname(options.htmlPath) : ".";
|
|
4946
3632
|
watch(watchPath, { recursive: true }, async (event, filename) => {
|
|
4947
3633
|
if (filename && (filename.endsWith(".html") || filename.endsWith(".js") || filename.endsWith(".css"))) {
|
|
4948
3634
|
console.log(`
|
|
@@ -4954,31 +3640,33 @@ cli.command("ios build", "Build iOS project").option("--html-path <path>", "Path
|
|
|
4954
3640
|
}
|
|
4955
3641
|
});
|
|
4956
3642
|
cli.command("ios open", "Open iOS project in Xcode").option("-o, --output <dir>", "iOS project directory", { default: "./ios" }).action(async (options) => {
|
|
4957
|
-
const iosModule = await
|
|
3643
|
+
const iosModule = await import(iosModuleURL);
|
|
4958
3644
|
await iosModule.open({
|
|
4959
3645
|
output: options?.output || "./ios"
|
|
4960
3646
|
});
|
|
4961
3647
|
});
|
|
4962
3648
|
cli.command("ios run", "Build and run on iOS device or simulator").option("-s, --simulator", "Run on simulator instead of device").option("-o, --output <dir>", "iOS project directory", { default: "./ios" }).example("craft ios run").example("craft ios run --simulator").action(async (options) => {
|
|
4963
|
-
const iosModule = await
|
|
3649
|
+
const iosModule = await import(iosModuleURL);
|
|
4964
3650
|
await iosModule.run({
|
|
4965
3651
|
simulator: options?.simulator || false,
|
|
4966
3652
|
output: options?.output || "./ios"
|
|
4967
3653
|
});
|
|
4968
3654
|
});
|
|
4969
|
-
cli.command("android init <name>", "Initialize a new Android project").option("--package <name>", "Package name (e.g., com.example.app)").option("-o, --output <dir>", "Output directory", { default: "./android" }).example("craft android init MyApp").example("craft android init MyApp --package com.example.myapp").action(async (name, options) => {
|
|
4970
|
-
const androidModule = await
|
|
3655
|
+
cli.command("android init <name>", "Initialize a new Android project").option("--package <name>", "Package name (e.g., com.example.app)").option("--renderer <kind>", "Android renderer: web (default) or native").option("-o, --output <dir>", "Output directory", { default: "./android" }).example("craft android init MyApp").example("craft android init MyApp --package com.example.myapp").action(async (name, options) => {
|
|
3656
|
+
const androidModule = await import(androidModuleURL);
|
|
4971
3657
|
await androidModule.init({
|
|
4972
3658
|
name,
|
|
4973
3659
|
packageName: options?.package,
|
|
4974
|
-
output: options?.output || "./android"
|
|
3660
|
+
output: options?.output || "./android",
|
|
3661
|
+
config: options?.renderer ? { renderer: options.renderer } : undefined
|
|
4975
3662
|
});
|
|
4976
3663
|
});
|
|
4977
|
-
cli.command("android build", "Build Android project").option("--html-path <path>", "Path to HTML file").option("-d, --dev-server <url>", "Development server URL").option("-o, --output <dir>", "Android project directory", { default: "./android" }).option("--release", "Build release APK").option("-w, --watch", "Watch for file changes and rebuild").example("craft android build").example("craft android build --release").example("craft android build --watch").action(async (options) => {
|
|
4978
|
-
const androidModule = await
|
|
3664
|
+
cli.command("android build", "Build Android project").option("--html-path <path>", "Path to HTML file").option("--native-bundle <path>", "JavaScript bundle compiled by stx-native").option("-d, --dev-server <url>", "Development server URL").option("-o, --output <dir>", "Android project directory", { default: "./android" }).option("--release", "Build release APK").option("-w, --watch", "Watch for file changes and rebuild").example("craft android build").example("craft android build --release").example("craft android build --native-bundle ./screen.js").example("craft android build --watch").action(async (options) => {
|
|
3665
|
+
const androidModule = await import(androidModuleURL);
|
|
4979
3666
|
const doBuild = async () => {
|
|
4980
3667
|
await androidModule.build({
|
|
4981
3668
|
htmlPath: options?.htmlPath,
|
|
3669
|
+
nativeBundlePath: options?.nativeBundle,
|
|
4982
3670
|
devServer: options?.devServer,
|
|
4983
3671
|
output: options?.output || "./android",
|
|
4984
3672
|
release: options?.release || false
|
|
@@ -4990,8 +3678,8 @@ cli.command("android build", "Build Android project").option("--html-path <path>
|
|
|
4990
3678
|
\uD83D\uDC40 Watching for changes...
|
|
4991
3679
|
`);
|
|
4992
3680
|
const { watch } = await import("fs");
|
|
4993
|
-
const { dirname
|
|
4994
|
-
const watchPath = options?.htmlPath ?
|
|
3681
|
+
const { dirname } = await import("path");
|
|
3682
|
+
const watchPath = options?.htmlPath ? dirname(options.htmlPath) : ".";
|
|
4995
3683
|
watch(watchPath, { recursive: true }, async (event, filename) => {
|
|
4996
3684
|
if (filename && (filename.endsWith(".html") || filename.endsWith(".js") || filename.endsWith(".css"))) {
|
|
4997
3685
|
console.log(`
|
|
@@ -5003,13 +3691,13 @@ cli.command("android build", "Build Android project").option("--html-path <path>
|
|
|
5003
3691
|
}
|
|
5004
3692
|
});
|
|
5005
3693
|
cli.command("android open", "Open Android project in Android Studio").option("-o, --output <dir>", "Android project directory", { default: "./android" }).action(async (options) => {
|
|
5006
|
-
const androidModule = await
|
|
3694
|
+
const androidModule = await import(androidModuleURL);
|
|
5007
3695
|
await androidModule.open({
|
|
5008
3696
|
output: options?.output || "./android"
|
|
5009
3697
|
});
|
|
5010
3698
|
});
|
|
5011
3699
|
cli.command("android run", "Build and run on Android device or emulator").option("-d, --device <id>", "Target device ID").option("-o, --output <dir>", "Android project directory", { default: "./android" }).example("craft android run").example("craft android run --device emulator-5554").action(async (options) => {
|
|
5012
|
-
const androidModule = await
|
|
3700
|
+
const androidModule = await import(androidModuleURL);
|
|
5013
3701
|
await androidModule.run({
|
|
5014
3702
|
device: options?.device,
|
|
5015
3703
|
output: options?.output || "./android"
|
|
@@ -5036,30 +3724,30 @@ cli.command("preview [path]", "Preview app in browser before building native").o
|
|
|
5036
3724
|
});
|
|
5037
3725
|
});
|
|
5038
3726
|
cli.command("publish", "Publish app to App Store or Play Store").option("--ios", "Publish to App Store (TestFlight)").option("--android", "Publish to Play Store").option("--api-key <path>", "App Store Connect API key path").option("--service-account <path>", "Google Play service account JSON").option("-o, --output <dir>", "Project directory", { default: "." }).example("craft publish --ios").example("craft publish --android").action(async (options) => {
|
|
5039
|
-
const { existsSync:
|
|
5040
|
-
const { join:
|
|
5041
|
-
const {
|
|
3727
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
3728
|
+
const { join: join2 } = await import("path");
|
|
3729
|
+
const { $ } = await Promise.resolve(globalThis.Bun);
|
|
5042
3730
|
if (options?.ios) {
|
|
5043
3731
|
console.log(`
|
|
5044
3732
|
\uD83D\uDCF1 Publishing to App Store (TestFlight)...
|
|
5045
3733
|
`);
|
|
5046
|
-
const iosDir =
|
|
5047
|
-
if (!
|
|
3734
|
+
const iosDir = join2(options?.output || ".", "ios");
|
|
3735
|
+
if (!existsSync5(iosDir)) {
|
|
5048
3736
|
console.error("\u274C No iOS project found. Run: craft ios init");
|
|
5049
3737
|
process6.exit(1);
|
|
5050
3738
|
}
|
|
5051
3739
|
console.log("1. Building archive...");
|
|
5052
3740
|
try {
|
|
5053
|
-
await
|
|
3741
|
+
await $`cd ${iosDir} && xcodebuild -scheme App -configuration Release -archivePath ./build/App.xcarchive archive`;
|
|
5054
3742
|
console.log("\u2705 Archive created");
|
|
5055
3743
|
console.log("2. Exporting IPA...");
|
|
5056
|
-
await
|
|
3744
|
+
await $`cd ${iosDir} && xcodebuild -exportArchive -archivePath ./build/App.xcarchive -exportPath ./build -exportOptionsPlist ExportOptions.plist`;
|
|
5057
3745
|
console.log("\u2705 IPA exported");
|
|
5058
3746
|
console.log("3. Uploading to TestFlight...");
|
|
5059
3747
|
if (options?.apiKey) {
|
|
5060
|
-
await
|
|
3748
|
+
await $`xcrun altool --upload-app -f ${iosDir}/build/*.ipa --apiKey ${options.apiKey} --type ios`;
|
|
5061
3749
|
} else {
|
|
5062
|
-
await
|
|
3750
|
+
await $`xcrun altool --upload-app -f ${iosDir}/build/*.ipa --type ios`;
|
|
5063
3751
|
}
|
|
5064
3752
|
console.log("\u2705 Uploaded to TestFlight!");
|
|
5065
3753
|
} catch (error) {
|
|
@@ -5073,16 +3761,16 @@ cli.command("publish", "Publish app to App Store or Play Store").option("--ios",
|
|
|
5073
3761
|
console.log(`
|
|
5074
3762
|
\uD83E\uDD16 Publishing to Play Store...
|
|
5075
3763
|
`);
|
|
5076
|
-
const androidDir =
|
|
5077
|
-
if (!
|
|
3764
|
+
const androidDir = join2(options?.output || ".", "android");
|
|
3765
|
+
if (!existsSync5(androidDir)) {
|
|
5078
3766
|
console.error("\u274C No Android project found. Run: craft android init");
|
|
5079
3767
|
process6.exit(1);
|
|
5080
3768
|
}
|
|
5081
3769
|
console.log("1. Building release AAB...");
|
|
5082
3770
|
try {
|
|
5083
|
-
await
|
|
3771
|
+
await $`cd ${androidDir} && ./gradlew bundleRelease`;
|
|
5084
3772
|
console.log("\u2705 AAB created");
|
|
5085
|
-
const aabPath =
|
|
3773
|
+
const aabPath = join2(androidDir, "app/build/outputs/bundle/release/app-release.aab");
|
|
5086
3774
|
if (options?.serviceAccount) {
|
|
5087
3775
|
console.log("2. Uploading to Play Store...");
|
|
5088
3776
|
console.log("\u26A0\uFE0F Automatic Play Store upload requires fastlane or Google Play API integration.");
|
|
@@ -5121,31 +3809,31 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
|
|
|
5121
3809
|
`);
|
|
5122
3810
|
const bundleId = options?.bundleId || `com.example.${name.toLowerCase().replace(/[^a-z0-9]/g, "")}`;
|
|
5123
3811
|
const appNameSlug = name.toLowerCase().replace(/[^a-z0-9]/g, "-");
|
|
5124
|
-
const { mkdirSync:
|
|
5125
|
-
const { join:
|
|
3812
|
+
const { mkdirSync: mkdirSync2, writeFileSync: writeFileSync3, existsSync: existsSync5, readdirSync: readdirSync2, readFileSync: readFileSync3, cpSync: cpSync2 } = await import("fs");
|
|
3813
|
+
const { join: join2, dirname } = await import("path");
|
|
5126
3814
|
const replaceVars = (content) => {
|
|
5127
3815
|
return content.replace(/\{\{APP_NAME\}\}/g, name).replace(/\{\{APP_NAME_SLUG\}\}/g, appNameSlug).replace(/\{\{BUNDLE_ID\}\}/g, bundleId).replace(/\{\{AUTHOR\}\}/g, "Developer");
|
|
5128
3816
|
};
|
|
5129
3817
|
const copyTemplate = async (templateName, destDir) => {
|
|
5130
|
-
const templateDir =
|
|
5131
|
-
if (!
|
|
3818
|
+
const templateDir = join2(import.meta.dir, "templates/projects", templateName);
|
|
3819
|
+
if (!existsSync5(templateDir)) {
|
|
5132
3820
|
console.log(` \u26A0\uFE0F Template '${templateName}' not found, using blank template`);
|
|
5133
3821
|
return false;
|
|
5134
3822
|
}
|
|
5135
3823
|
const copyRecursive = (src, dest) => {
|
|
5136
|
-
if (!
|
|
5137
|
-
|
|
3824
|
+
if (!existsSync5(dest)) {
|
|
3825
|
+
mkdirSync2(dest, { recursive: true });
|
|
5138
3826
|
}
|
|
5139
|
-
const entries =
|
|
3827
|
+
const entries = readdirSync2(src, { withFileTypes: true });
|
|
5140
3828
|
for (const entry of entries) {
|
|
5141
|
-
const srcPath =
|
|
5142
|
-
const destPath =
|
|
3829
|
+
const srcPath = join2(src, entry.name);
|
|
3830
|
+
const destPath = join2(dest, entry.name);
|
|
5143
3831
|
if (entry.isDirectory()) {
|
|
5144
3832
|
copyRecursive(srcPath, destPath);
|
|
5145
3833
|
} else {
|
|
5146
|
-
const content =
|
|
3834
|
+
const content = readFileSync3(srcPath, "utf-8");
|
|
5147
3835
|
const processedContent = replaceVars(content);
|
|
5148
|
-
|
|
3836
|
+
writeFileSync3(destPath, processedContent);
|
|
5149
3837
|
}
|
|
5150
3838
|
}
|
|
5151
3839
|
};
|
|
@@ -5154,13 +3842,13 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
|
|
|
5154
3842
|
};
|
|
5155
3843
|
if (["blank", "tabs", "drawer", "dashboard"].includes(template)) {
|
|
5156
3844
|
console.log(`\uD83D\uDCC1 Creating ${template} project from template...`);
|
|
5157
|
-
if (!
|
|
5158
|
-
|
|
3845
|
+
if (!existsSync5(name)) {
|
|
3846
|
+
mkdirSync2(name, { recursive: true });
|
|
5159
3847
|
}
|
|
5160
3848
|
const copied = await copyTemplate(template, name);
|
|
5161
3849
|
if (!copied) {
|
|
5162
|
-
|
|
5163
|
-
|
|
3850
|
+
mkdirSync2(join2(name, "src"), { recursive: true });
|
|
3851
|
+
writeFileSync3(join2(name, "index.html"), replaceVars(`<!DOCTYPE html>
|
|
5164
3852
|
<html lang="en">
|
|
5165
3853
|
<head>
|
|
5166
3854
|
<meta charset="UTF-8">
|
|
@@ -5174,7 +3862,7 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
|
|
|
5174
3862
|
</div>
|
|
5175
3863
|
</body>
|
|
5176
3864
|
</html>`));
|
|
5177
|
-
|
|
3865
|
+
writeFileSync3(join2(name, "package.json"), replaceVars(JSON.stringify({
|
|
5178
3866
|
name: "{{APP_NAME_SLUG}}",
|
|
5179
3867
|
version: "1.0.0",
|
|
5180
3868
|
private: true,
|
|
@@ -5188,13 +3876,13 @@ cli.command("init <name>", "Initialize a new Craft project").option("--template
|
|
|
5188
3876
|
}
|
|
5189
3877
|
}, null, 2)));
|
|
5190
3878
|
}
|
|
5191
|
-
pinCraftNativeDependency(
|
|
3879
|
+
pinCraftNativeDependency(join2(name, "package.json"), version);
|
|
5192
3880
|
console.log(`\u2705 ${template} project created`);
|
|
5193
3881
|
}
|
|
5194
3882
|
if (template === "desktop" || template === "all") {
|
|
5195
3883
|
console.log("\uD83D\uDCC1 Creating desktop project structure...");
|
|
5196
|
-
if (!
|
|
5197
|
-
|
|
3884
|
+
if (!existsSync5(name)) {
|
|
3885
|
+
mkdirSync2(name, { recursive: true });
|
|
5198
3886
|
}
|
|
5199
3887
|
const configContent = `import type { CraftConfig } from 'craft-native'
|
|
5200
3888
|
|
|
@@ -5208,7 +3896,7 @@ export default {
|
|
|
5208
3896
|
},
|
|
5209
3897
|
} satisfies CraftConfig
|
|
5210
3898
|
`;
|
|
5211
|
-
|
|
3899
|
+
writeFileSync3(`${name}/craft.config.ts`, configContent);
|
|
5212
3900
|
const htmlContent = `<!DOCTYPE html>
|
|
5213
3901
|
<html lang="en">
|
|
5214
3902
|
<head>
|
|
@@ -5239,7 +3927,7 @@ export default {
|
|
|
5239
3927
|
</body>
|
|
5240
3928
|
</html>
|
|
5241
3929
|
`;
|
|
5242
|
-
|
|
3930
|
+
writeFileSync3(`${name}/index.html`, htmlContent);
|
|
5243
3931
|
const packageJson = {
|
|
5244
3932
|
name: appNameSlug,
|
|
5245
3933
|
version: "0.1.0",
|
|
@@ -5255,13 +3943,13 @@ export default {
|
|
|
5255
3943
|
"craft-native": `^${version}`
|
|
5256
3944
|
}
|
|
5257
3945
|
};
|
|
5258
|
-
|
|
3946
|
+
writeFileSync3(`${name}/package.json`, JSON.stringify(packageJson, null, 2));
|
|
5259
3947
|
pinCraftNativeDependency(`${name}/package.json`, version);
|
|
5260
3948
|
console.log("\u2705 Desktop project created");
|
|
5261
3949
|
}
|
|
5262
3950
|
if (template === "ios" || template === "all") {
|
|
5263
3951
|
console.log("\uD83D\uDCF1 Creating iOS project...");
|
|
5264
|
-
const iosModule = await
|
|
3952
|
+
const iosModule = await import(iosModuleURL);
|
|
5265
3953
|
await iosModule.init({
|
|
5266
3954
|
name,
|
|
5267
3955
|
bundleId: options?.bundleId,
|
|
@@ -5271,7 +3959,7 @@ export default {
|
|
|
5271
3959
|
}
|
|
5272
3960
|
if (template === "android" || template === "all") {
|
|
5273
3961
|
console.log("\uD83E\uDD16 Creating Android project...");
|
|
5274
|
-
const androidModule = await
|
|
3962
|
+
const androidModule = await import(androidModuleURL);
|
|
5275
3963
|
await androidModule.init({
|
|
5276
3964
|
name,
|
|
5277
3965
|
packageName: options?.bundleId,
|
|
@@ -5298,4 +3986,7 @@ export default {
|
|
|
5298
3986
|
});
|
|
5299
3987
|
cli.version(version);
|
|
5300
3988
|
cli.help();
|
|
3989
|
+
if ((process6.argv[2] === "ios" || process6.argv[2] === "android") && ["init", "build", "open", "run"].includes(process6.argv[3])) {
|
|
3990
|
+
process6.argv.splice(2, 2, `${process6.argv[2]} ${process6.argv[3]}`);
|
|
3991
|
+
}
|
|
5301
3992
|
cli.parse();
|