@akanjs/devkit 3.0.0-beta.9 → 3.0.0
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/DEV_RUNTIME_KNOBS.md +44 -26
- package/README.ko.md +21 -7
- package/README.md +17 -8
- package/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +6 -8
- package/agentsIndex.test.ts +4 -7
- package/agentsIndex.ts +6 -22
- package/akanApp/BackendImportGraph.test.ts +8 -22
- package/akanApp/BackendImportGraph.ts +1 -20
- package/akanApp/akanApp.host.test.ts +56 -3
- package/akanApp/akanApp.host.ts +218 -278
- package/akanApp/devBootLatch.test.ts +39 -0
- package/akanApp/devBootLatch.ts +39 -0
- package/akanApp/devHostPolicy.test.ts +81 -35
- package/akanApp/devHostPolicy.ts +41 -102
- package/akanConfig/akanBin.test.ts +62 -0
- package/akanConfig/akanBin.ts +78 -0
- package/akanConfig/akanConfig.test.ts +439 -141
- package/akanConfig/akanConfig.ts +359 -275
- package/akanConfig/index.ts +1 -0
- package/akanConfig/types.ts +22 -8
- package/akanContext.ts +110 -160
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.test.ts +41 -5
- package/applicationBuildRunner.ts +71 -63
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +32 -30
- package/artifact/implicitRootLayout.ts +58 -106
- package/artifact/routeSeedIndex.test.ts +7 -6
- package/artifact/routeSeedIndex.ts +11 -44
- package/biome.base.json +35 -2
- package/biomeBase.ts +2 -5
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +77 -39
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +26 -5
- package/cloud/globalConfig.ts +71 -32
- package/codeAgent/agent/AkanCodeServices.ts +97 -0
- package/codeAgent/agent/AkanContextFiles.ts +94 -0
- package/codeAgent/agent/CodeAgent.ts +551 -0
- package/codeAgent/agent/CodeAgentAsks.ts +66 -0
- package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
- package/codeAgent/agent/CodeAgentGate.ts +93 -0
- package/codeAgent/agent/CodeAgentProxy.ts +82 -0
- package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
- package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
- package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
- package/codeAgent/agent/CodeAgentUi.ts +47 -0
- package/codeAgent/agent/CodeMailbox.ts +148 -0
- package/codeAgent/agent/CodeSessionFork.ts +56 -0
- package/codeAgent/agent/CodeSessionIndex.ts +78 -0
- package/codeAgent/agent/SubagentPool.ts +214 -0
- package/codeAgent/agent/akanCodeModel.ts +47 -0
- package/codeAgent/agent/akanCodePaths.ts +31 -0
- package/codeAgent/agent/akanContextFiles.test.ts +114 -0
- package/codeAgent/agent/akanSystemPrompt.ts +46 -0
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +523 -0
- package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
- package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
- package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
- package/codeAgent/agent/codeMailbox.test.ts +76 -0
- package/codeAgent/agent/codeSessionFork.test.ts +58 -0
- package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
- package/codeAgent/agent/consoleToStderr.ts +8 -0
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +82 -0
- package/codeAgent/feedback/PreviewChrome.ts +82 -0
- package/codeAgent/feedback/PreviewFeedback.ts +46 -0
- package/codeAgent/feedback/PreviewView.ts +95 -0
- package/codeAgent/feedback/TurnFeedback.ts +98 -0
- package/codeAgent/feedback/VerifyFeedback.ts +40 -0
- package/codeAgent/feedback/previewChrome.test.ts +73 -0
- package/codeAgent/index.ts +34 -0
- package/codeAgent/skills/akan-code/SKILL.md +130 -0
- package/codeAgent/skills/akan-debug/SKILL.md +42 -0
- package/codeAgent/skills/akan-deps/SKILL.md +50 -0
- package/codeAgent/skills/akan-err/SKILL.md +39 -0
- package/codeAgent/skills/akan-module/SKILL.md +45 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
- package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
- package/codeAgent/skills/akan-service/SKILL.md +34 -0
- package/codeAgent/skills/akan-store/SKILL.md +67 -0
- package/codeAgent/skills/akan-ui/SKILL.md +63 -0
- package/codeAgent/skills/akan-validate/SKILL.md +44 -0
- package/codeAgent/tools/AkanCli.ts +50 -0
- package/codeAgent/tools/AkanCodePlugins.ts +136 -0
- package/codeAgent/tools/AkanEditScope.ts +48 -0
- package/codeAgent/tools/AkanEnvKeys.ts +50 -0
- package/codeAgent/tools/AkanToolPack.ts +92 -0
- package/codeAgent/tools/AkanVerifier.ts +48 -0
- package/codeAgent/tools/AskToolPack.ts +99 -0
- package/codeAgent/tools/MailToolPack.ts +76 -0
- package/codeAgent/tools/McpClient.ts +187 -0
- package/codeAgent/tools/McpOAuth.ts +233 -0
- package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
- package/codeAgent/tools/McpServerConfig.ts +129 -0
- package/codeAgent/tools/McpSignIn.ts +158 -0
- package/codeAgent/tools/McpTokenStore.ts +81 -0
- package/codeAgent/tools/McpToolPack.ts +199 -0
- package/codeAgent/tools/SessionToolPack.ts +91 -0
- package/codeAgent/tools/WebToolPack.ts +147 -0
- package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
- package/codeAgent/tools/askToolPack.test.ts +116 -0
- package/codeAgent/tools/mcpOAuth.test.ts +612 -0
- package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
- package/codegenLock.test.ts +4 -13
- package/codegenLock.ts +3 -17
- package/commandDecorators/argMeta.ts +1 -5
- package/commandDecorators/camelToKebabCase.ts +1 -0
- package/commandDecorators/command.ts +46 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +2 -10
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +68 -145
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/csrE2e/csrE2eHarness.fixture.ts +505 -0
- package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
- package/csrE2e/csrPageStack.e2e.test.ts +252 -0
- package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
- package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
- package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
- package/csrE2e/registryVendor.e2e.test.ts +221 -0
- package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +31 -80
- package/devkitUtils.test.ts +48 -20
- package/executors.linkFallback.test.ts +103 -0
- package/executors.test.ts +176 -106
- package/executors.ts +212 -498
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +5 -3
- package/fileSys.ts +22 -9
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +27 -38
- package/frontendBuild/autoImportSync.ts +31 -65
- package/frontendBuild/buildRouteClient.test.ts +123 -37
- package/frontendBuild/bundleDefine.test.ts +12 -0
- package/frontendBuild/bundleDefine.ts +16 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
- package/frontendBuild/clientEntryDiscovery.ts +122 -110
- package/frontendBuild/csrArtifactBuilder.ts +9 -31
- package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
- package/frontendBuild/csrDevArtifactWriter.ts +307 -0
- package/frontendBuild/csrDevBundler.test.ts +66 -0
- package/frontendBuild/csrDevBundler.ts +95 -0
- package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
- package/frontendBuild/csrDevFactoryCheck.ts +39 -0
- package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
- package/frontendBuild/csrDevModuleCompiler.ts +422 -0
- package/frontendBuild/csrDevPatcher.test.ts +572 -0
- package/frontendBuild/csrDevPatcher.ts +548 -0
- package/frontendBuild/csrDevPaths.ts +145 -0
- package/frontendBuild/csrDevResolver.test.ts +239 -0
- package/frontendBuild/csrDevResolver.ts +363 -0
- package/frontendBuild/csrDevTypes.ts +61 -0
- package/frontendBuild/csrEntryFiles.ts +24 -0
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +31 -82
- package/frontendBuild/cssImportResolver.ts +6 -19
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/devRegistryBundler.ts +192 -0
- package/frontendBuild/fontOptimizer.test.ts +6 -14
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +57 -70
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +59 -32
- package/frontendBuild/hmrWatcher.ts +31 -74
- package/frontendBuild/index.ts +4 -0
- package/frontendBuild/pagesBundleBuilder.ts +28 -30
- package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
- package/frontendBuild/registryMetadataFingerprint.ts +28 -0
- package/frontendBuild/routeClientBuilder.ts +100 -62
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/serverGraphFile.test.ts +77 -0
- package/frontendBuild/serverGraphFile.ts +93 -0
- package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
- package/frontendBuild/ssrDevBundler.ts +85 -0
- package/frontendBuild/styleContract.ts +1 -8
- package/frontendBuild/themeValidator.ts +4 -26
- package/frontendBuild/watchRootResolver.test.ts +23 -42
- package/frontendBuild/watchRootResolver.ts +5 -27
- package/getRelatedCnsts.ts +12 -92
- package/incrementalBuilder/buildBatch.proc.ts +147 -63
- package/incrementalBuilder/buildBatchProtocol.ts +13 -38
- package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
- package/incrementalBuilder/buildBatchRunner.ts +26 -38
- package/incrementalBuilder/builderChannel.test.ts +24 -49
- package/incrementalBuilder/builderChannel.ts +6 -37
- package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
- package/incrementalBuilder/builderRequestRouter.ts +5 -24
- package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
- package/incrementalBuilder/builderWorkQueue.ts +126 -0
- package/incrementalBuilder/devWatchBatch.test.ts +21 -24
- package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
- package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +270 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
- package/index.ts +5 -19
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +105 -199
- package/integration/devStabilityHarness.test.ts +13 -28
- package/integration/devStabilityHarness.ts +65 -330
- package/integration/ssrMemoryProbe.ts +41 -167
- package/libSource.test.ts +4 -15
- package/libSource.ts +19 -31
- package/lint/__fixtures__/no-document-cookie/bad.tsx +4 -0
- package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
- package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
- package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
- package/lint/gritRules.test.ts +8 -24
- package/lint/no-deep-internal-import.grit +2 -2
- package/lint/no-document-cookie.grit +29 -0
- package/lint/no-redeclare-predefined-endpoint.grit +2 -1
- package/lint/no-web-only-api-outside-webkit.grit +44 -0
- package/lint/no-web-storage.grit +30 -0
- package/linter.test.ts +1 -2
- package/linter.ts +26 -116
- package/mobile/desktopBin.test.ts +182 -0
- package/mobile/desktopBin.ts +144 -0
- package/mobile/desktopServerStage.test.ts +252 -0
- package/mobile/desktopServerStage.ts +178 -0
- package/mobile/index.ts +9 -0
- package/mobile/mobileTarget.test.ts +57 -8
- package/mobile/mobileTarget.ts +45 -26
- package/mobile/nativeApi.test.ts +30 -0
- package/mobile/nativeApi.ts +40 -0
- package/mobile/nativeApp.test.ts +184 -0
- package/mobile/nativeApp.ts +454 -0
- package/mobile/nativeAppLine.test.ts +69 -0
- package/mobile/nativeAppLine.ts +71 -0
- package/mobile/nativeConfig.test.ts +438 -0
- package/mobile/nativeConfig.ts +294 -0
- package/mobile/nativePluginFolders.test.ts +81 -0
- package/mobile/nativePluginFolders.ts +60 -0
- package/mobile/nativeWebDir.test.ts +80 -0
- package/mobile/nativeWebDir.ts +66 -0
- package/mobile/placeholderAppId.test.ts +13 -0
- package/mobile/placeholderAppId.ts +16 -0
- package/mobile/usageDescriptions.test.ts +27 -0
- package/mobile/usageDescriptions.ts +24 -0
- package/package.json +11 -11
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +46 -0
- package/platformTest/BunTestReport.test.ts +60 -0
- package/platformTest/BunTestReport.ts +65 -0
- package/platformTest/IdleWatchedProcess.test.ts +56 -0
- package/platformTest/IdleWatchedProcess.ts +73 -0
- package/platformTest/LinuxDockerTarget.ts +105 -0
- package/platformTest/PlatformTestRun.test.ts +64 -0
- package/platformTest/PlatformTestRun.ts +141 -0
- package/platformTest/PlatformTestTarget.ts +142 -0
- package/platformTest/TestSnapshot.test.ts +72 -0
- package/platformTest/TestSnapshot.ts +103 -0
- package/platformTest/WindowsSshTarget.ts +151 -0
- package/prompter.ts +3 -13
- package/qualityScanner.test.ts +9 -26
- package/qualityScanner.ts +200 -286
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.test.ts +0 -1
- package/routeSourceValidator.ts +7 -42
- package/scanInfo.ts +91 -225
- package/selectModel.ts +1 -2
- package/semver.ts +3 -5
- package/slicePlanner.test.ts +6 -24
- package/slicePlanner.ts +11 -32
- package/spinner.test.ts +3 -8
- package/spinner.ts +4 -16
- package/sshTunnel.ts +1 -6
- package/ssrScanner.test.ts +1 -6
- package/ssrScanner.ts +103 -113
- package/stripAnsi.ts +3 -0
- package/subspace.test.ts +9 -25
- package/subspace.ts +48 -115
- package/subspaceConfig.ts +3 -11
- package/testHelpers.ts +194 -0
- package/transforms/asyncDefaultExportDetector.ts +3 -11
- package/transforms/barrelAnalyzer.ts +43 -135
- package/transforms/barrelImportsPlugin.ts +38 -175
- package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
- package/transforms/externalizeFrameworkPlugin.ts +13 -87
- package/transforms/moduleSyntax.ts +32 -0
- package/transforms/rscUseClientTransform.ts +42 -20
- package/transforms/serverEnvPlugin.test.ts +103 -0
- package/transforms/serverEnvPlugin.ts +41 -0
- package/transforms/transforms.test.ts +166 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +6 -23
- package/typeChecker.ts +7 -63
- package/types.ts +2 -0
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +101 -110
- package/workflow/index.ts +0 -1
- package/workflow/moduleIndex.test.ts +4 -15
- package/workflow/moduleIndex.ts +9 -49
- package/workflow/plan.ts +18 -25
- package/workflow/render.ts +80 -96
- package/workflow/rolloutGate.test.ts +1 -1
- package/workflow/source.ts +52 -69
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.test.ts +18 -3
- package/workspaceLayout.ts +22 -23
- package/abstractCompactor.ts +0 -112
- package/aiEditor.test.ts +0 -60
- package/aiEditor.ts +0 -387
- package/applicationReleasePackager.ts +0 -209
- package/capacitorApp.test.ts +0 -372
- package/capacitorApp.ts +0 -1648
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/guideline.ts +0 -19
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
package/capacitorApp.ts
DELETED
|
@@ -1,1648 +0,0 @@
|
|
|
1
|
-
import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import type { CapacitorConfig } from "@capacitor/cli";
|
|
5
|
-
import { select } from "@inquirer/prompts";
|
|
6
|
-
import { MobileProject } from "@trapezedev/project";
|
|
7
|
-
import type { AndroidProject } from "@trapezedev/project/dist/android/project";
|
|
8
|
-
import type { IosProject } from "@trapezedev/project/dist/ios/project";
|
|
9
|
-
import type { AkanNativeContext, AkanPlugin } from "akanjs";
|
|
10
|
-
import { capitalize } from "akanjs/common";
|
|
11
|
-
import type { AkanMobileTargetConfig, MobilePermission } from "./akanConfig";
|
|
12
|
-
import { type AppExecutor, CommandExecutionError } from "./executors";
|
|
13
|
-
import { FileEditor } from "./fileEditor";
|
|
14
|
-
import { resolveMobilePath, targetHtmlFilename } from "./mobile";
|
|
15
|
-
|
|
16
|
-
interface RunConfig {
|
|
17
|
-
operation: "local" | "release";
|
|
18
|
-
env: "local" | "debug" | "develop" | "main";
|
|
19
|
-
regenerate?: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
interface RunIosConfig extends RunConfig {
|
|
23
|
-
noAllowProvisioningUpdates?: boolean;
|
|
24
|
-
iosDeviceId?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
interface PrepareConfig extends RunConfig {
|
|
28
|
-
iosRunTargetKind?: IosRunTargetKind;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
type MobileCommandEnv = Record<string, string | undefined>;
|
|
32
|
-
|
|
33
|
-
export type IosRunTargetKind = "device" | "simulator";
|
|
34
|
-
export interface IosRunTarget {
|
|
35
|
-
id: string;
|
|
36
|
-
name: string;
|
|
37
|
-
kind: IosRunTargetKind;
|
|
38
|
-
state?: string;
|
|
39
|
-
/** Display runtime for simulators, e.g. "iOS 18.2". Undefined for physical devices. */
|
|
40
|
-
runtime?: string;
|
|
41
|
-
devicectlId?: string;
|
|
42
|
-
xcodebuildId?: string;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export interface IosNativeRunCommand {
|
|
46
|
-
xcodebuildArgs: string[];
|
|
47
|
-
appPath: string;
|
|
48
|
-
configuration: "Debug" | "Release";
|
|
49
|
-
derivedDataPath: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export type IosRunFailureKind =
|
|
53
|
-
| "apple-account"
|
|
54
|
-
| "bundle-identifier"
|
|
55
|
-
| "compiler-toolchain"
|
|
56
|
-
| "team-permission"
|
|
57
|
-
| "license-agreement"
|
|
58
|
-
| "certificate"
|
|
59
|
-
| "provisioning-profile"
|
|
60
|
-
| "device-registration"
|
|
61
|
-
| "device-state"
|
|
62
|
-
| "devicectl-unavailable"
|
|
63
|
-
| "simulator-runtime"
|
|
64
|
-
| "unknown";
|
|
65
|
-
|
|
66
|
-
// Recent iOS SDKs (iOS 18+) split SwiftUI into a SwiftUICore dylib that does not exist on older
|
|
67
|
-
// runtimes, so an app built against them dyld-crashes at launch on an iOS <18 simulator/device.
|
|
68
|
-
export const SWIFTUICORE_MIN_IOS_MAJOR = 18;
|
|
69
|
-
|
|
70
|
-
export interface IosRunFailureClassification {
|
|
71
|
-
kind: IosRunFailureKind;
|
|
72
|
-
title: string;
|
|
73
|
-
detail: string;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const iosNativeBlockedEnvKeys = new Set([
|
|
77
|
-
"AR",
|
|
78
|
-
"AS",
|
|
79
|
-
"CC",
|
|
80
|
-
"CFLAGS",
|
|
81
|
-
"CONDA_BUILD_SYSROOT",
|
|
82
|
-
"CONDA_PREFIX",
|
|
83
|
-
"CPP",
|
|
84
|
-
"CPPFLAGS",
|
|
85
|
-
"CPATH",
|
|
86
|
-
"CXX",
|
|
87
|
-
"CXXFLAGS",
|
|
88
|
-
"LD",
|
|
89
|
-
"LDFLAGS",
|
|
90
|
-
"LIBRARY_PATH",
|
|
91
|
-
"MACOSX_DEPLOYMENT_TARGET",
|
|
92
|
-
"NM",
|
|
93
|
-
"OBJC",
|
|
94
|
-
"OBJCXX",
|
|
95
|
-
"PREFIX",
|
|
96
|
-
"RANLIB",
|
|
97
|
-
"SDKROOT",
|
|
98
|
-
"STRIP",
|
|
99
|
-
]);
|
|
100
|
-
|
|
101
|
-
export const rootCapacitorConfigFilenames = [
|
|
102
|
-
"capacitor.config.ts",
|
|
103
|
-
"capacitor.config.js",
|
|
104
|
-
"capacitor.config.json",
|
|
105
|
-
] as const;
|
|
106
|
-
|
|
107
|
-
export const rootCapacitorConfigPaths = (appRoot: string) =>
|
|
108
|
-
rootCapacitorConfigFilenames.map((file) => path.join(appRoot, file));
|
|
109
|
-
|
|
110
|
-
export async function clearRootCapacitorConfigs(appRoot: string) {
|
|
111
|
-
await Promise.all(rootCapacitorConfigPaths(appRoot).map((file) => rm(file, { force: true })));
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export async function writeRootCapacitorConfig(appRoot: string, content: string) {
|
|
115
|
-
await clearRootCapacitorConfigs(appRoot);
|
|
116
|
-
await Bun.write(path.join(appRoot, "capacitor.config.json"), content);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
interface MaterializeCapacitorConfigOptions {
|
|
120
|
-
operation: RunConfig["operation"];
|
|
121
|
-
localServerUrl?: string;
|
|
122
|
-
localIp?: string;
|
|
123
|
-
}
|
|
124
|
-
type MobilePlatform = "ios" | "android";
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* `AppExecutor.spawn`'s own options plus the two the Capacitor config is written from before the spawn:
|
|
128
|
-
* the platform the command targets, and — for iOS — whether it targets a device or a simulator, which
|
|
129
|
-
* decide the `capacitor.config.json` that command reads.
|
|
130
|
-
*/
|
|
131
|
-
type SpawnMobileOptions = Parameters<AppExecutor["spawn"]>[2] & {
|
|
132
|
-
platform?: MobilePlatform;
|
|
133
|
-
iosRunTargetKind?: IosRunTargetKind;
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
export interface LocalDevHostResolution {
|
|
137
|
-
host: string;
|
|
138
|
-
source: "override" | "detected" | "loopback" | "platform";
|
|
139
|
-
candidates: { name: string; address: string }[];
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Interface-name prefixes that are almost never the routable LAN NIC a physical device can reach:
|
|
143
|
-
// bridges (Thunderbolt/USB), tunnels, AirDrop/awrl, VM/container virtual adapters.
|
|
144
|
-
const virtualInterfacePrefixes = [
|
|
145
|
-
"bridge",
|
|
146
|
-
"utun",
|
|
147
|
-
"llw",
|
|
148
|
-
"awdl",
|
|
149
|
-
"ap",
|
|
150
|
-
"vmnet",
|
|
151
|
-
"vnic",
|
|
152
|
-
"tap",
|
|
153
|
-
"tun",
|
|
154
|
-
"docker",
|
|
155
|
-
"veth",
|
|
156
|
-
"vboxnet",
|
|
157
|
-
"gif",
|
|
158
|
-
"stf",
|
|
159
|
-
];
|
|
160
|
-
const physicalInterfacePrefixes = ["en", "eth", "wlan", "wlp", "enp", "eno", "wlo"];
|
|
161
|
-
|
|
162
|
-
const isPrivateLanIpv4 = (address: string): boolean => {
|
|
163
|
-
if (address.startsWith("10.") || address.startsWith("192.168.")) return true;
|
|
164
|
-
const secondOctet = Number(address.match(/^172\.(\d+)\./)?.[1]);
|
|
165
|
-
return Number.isFinite(secondOctet) && secondOctet >= 16 && secondOctet <= 31;
|
|
166
|
-
};
|
|
167
|
-
|
|
168
|
-
// Higher = more likely to be the reachable LAN address. Link-local (169.254) is never routable;
|
|
169
|
-
// virtual/bridge interfaces are demoted below real NICs; private-LAN ranges get a small boost.
|
|
170
|
-
const scoreDevHostCandidate = (name: string, address: string): number => {
|
|
171
|
-
const lowerName = name.toLowerCase();
|
|
172
|
-
let score = 0;
|
|
173
|
-
if (address.startsWith("169.254.")) score -= 1000;
|
|
174
|
-
if (virtualInterfacePrefixes.some((prefix) => lowerName.startsWith(prefix))) score -= 100;
|
|
175
|
-
else if (physicalInterfacePrefixes.some((prefix) => lowerName.startsWith(prefix))) score += 100;
|
|
176
|
-
if (isPrivateLanIpv4(address)) score += 10;
|
|
177
|
-
return score;
|
|
178
|
-
};
|
|
179
|
-
|
|
180
|
-
// Pick the dev-server host a physical device should connect to. An explicit override always wins;
|
|
181
|
-
// otherwise rank non-internal IPv4 interfaces so a down/virtual interface (e.g. an inactive
|
|
182
|
-
// Thunderbolt bridge enumerated first) never shadows a real LAN NIC. Deterministic tie-break.
|
|
183
|
-
export const selectLocalDevHost = (
|
|
184
|
-
interfaces: NodeJS.Dict<os.NetworkInterfaceInfo[]>,
|
|
185
|
-
{ override }: { override?: string } = {},
|
|
186
|
-
): LocalDevHostResolution => {
|
|
187
|
-
const candidates: { name: string; address: string }[] = [];
|
|
188
|
-
for (const [name, aliases] of Object.entries(interfaces)) {
|
|
189
|
-
for (const alias of aliases ?? []) {
|
|
190
|
-
if (alias.family !== "IPv4" || alias.internal) continue;
|
|
191
|
-
candidates.push({ name, address: alias.address });
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
const trimmedOverride = override?.trim();
|
|
195
|
-
if (trimmedOverride) return { host: trimmedOverride, source: "override", candidates };
|
|
196
|
-
const [best] = [...candidates].sort(
|
|
197
|
-
(a, b) =>
|
|
198
|
-
scoreDevHostCandidate(b.name, b.address) - scoreDevHostCandidate(a.name, a.address) ||
|
|
199
|
-
a.name.localeCompare(b.name) ||
|
|
200
|
-
a.address.localeCompare(b.address),
|
|
201
|
-
);
|
|
202
|
-
return best
|
|
203
|
-
? { host: best.address, source: "detected", candidates }
|
|
204
|
-
: { host: "127.0.0.1", source: "loopback", candidates };
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
208
|
-
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
209
|
-
|
|
210
|
-
const asString = (value: unknown) => (typeof value === "string" ? value : undefined);
|
|
211
|
-
|
|
212
|
-
const firstString = (...values: unknown[]) => values.find((value): value is string => typeof value === "string");
|
|
213
|
-
|
|
214
|
-
const scoreIosDeviceTarget = (target: IosRunTarget) => {
|
|
215
|
-
const state = target.state?.toLowerCase() ?? "";
|
|
216
|
-
return (state.includes("available") ? 4 : 0) + (state.includes("wired") ? 2 : 0) + (state.includes("paired") ? 1 : 0);
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
const dedupeIosRunTargets = (targets: IosRunTarget[]) => {
|
|
220
|
-
const byKey = new Map<string, IosRunTarget>();
|
|
221
|
-
const runnableTargets = targets.filter((target) => !target.state?.toLowerCase().includes("unavailable"));
|
|
222
|
-
for (const target of runnableTargets) {
|
|
223
|
-
const key = target.xcodebuildId ?? target.id;
|
|
224
|
-
const current = byKey.get(key);
|
|
225
|
-
if (!current || scoreIosDeviceTarget(target) > scoreIosDeviceTarget(current)) byKey.set(key, target);
|
|
226
|
-
}
|
|
227
|
-
return [...byKey.values()];
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
// Normalize a simctl runtime — either the JSON key ("com.apple.CoreSimulator.SimRuntime.iOS-18-2")
|
|
231
|
-
// or a per-device `runtimeIdentifier` / text header ("iOS 18.2") — into a "iOS 18.2" display string.
|
|
232
|
-
const formatSimctlRuntime = (key?: string): string | undefined => {
|
|
233
|
-
if (!key) return undefined;
|
|
234
|
-
if (/^(iOS|watchOS|tvOS|visionOS)\s+[\d.]+$/i.test(key)) return key;
|
|
235
|
-
const match = key.match(/(iOS|watchOS|tvOS|visionOS)-(\d+)(?:-(\d+))?/i);
|
|
236
|
-
if (!match) return undefined;
|
|
237
|
-
return `${match[1]} ${match[2]}${match[3] ? `.${match[3]}` : ""}`;
|
|
238
|
-
};
|
|
239
|
-
|
|
240
|
-
// Extract the major OS version from a runtime display string ("iOS 18.2" → 18). Undefined for
|
|
241
|
-
// physical devices (no runtime) or unparseable values.
|
|
242
|
-
export const parseIosRuntimeMajor = (runtime?: string): number | undefined => {
|
|
243
|
-
const major = runtime?.match(/(\d+)/)?.[1];
|
|
244
|
-
if (major === undefined) return undefined;
|
|
245
|
-
const parsed = Number.parseInt(major, 10);
|
|
246
|
-
return Number.isNaN(parsed) ? undefined : parsed;
|
|
247
|
-
};
|
|
248
|
-
|
|
249
|
-
// Rank for default-target ordering: ready targets (booted simulator / connected device) first, then
|
|
250
|
-
// physical devices, then newer simulator runtimes. Fixes the old ascending-runtime order that made a
|
|
251
|
-
// stale iOS 17.x simulator the default pick.
|
|
252
|
-
const iosRunTargetRank = (target: IosRunTarget): number => {
|
|
253
|
-
const state = target.state?.toLowerCase() ?? "";
|
|
254
|
-
const ready = state.includes("booted") || state.includes("connected") || state.includes("available") ? 1000 : 0;
|
|
255
|
-
const device = target.kind === "device" ? 500 : 0;
|
|
256
|
-
return ready + device + (parseIosRuntimeMajor(target.runtime) ?? 0);
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
export const sortIosRunTargets = (targets: IosRunTarget[]): IosRunTarget[] =>
|
|
260
|
-
[...targets].sort((a, b) => iosRunTargetRank(b) - iosRunTargetRank(a) || a.name.localeCompare(b.name));
|
|
261
|
-
|
|
262
|
-
function walkRecords(value: unknown, visit: (record: Record<string, unknown>) => void) {
|
|
263
|
-
if (Array.isArray(value)) {
|
|
264
|
-
for (const item of value) walkRecords(item, visit);
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
if (!isRecord(value)) return;
|
|
268
|
-
visit(value);
|
|
269
|
-
for (const item of Object.values(value)) walkRecords(item, visit);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
export function parseDevicectlDevices(output: string): IosRunTarget[] {
|
|
273
|
-
try {
|
|
274
|
-
const json = JSON.parse(output) as unknown;
|
|
275
|
-
const targets = new Map<string, IosRunTarget>();
|
|
276
|
-
walkRecords(json, (record) => {
|
|
277
|
-
const deviceProperties = isRecord(record.deviceProperties) ? record.deviceProperties : {};
|
|
278
|
-
const hardwareProperties = isRecord(record.hardwareProperties) ? record.hardwareProperties : {};
|
|
279
|
-
const connectionProperties = isRecord(record.connectionProperties) ? record.connectionProperties : {};
|
|
280
|
-
const devicectlId = firstString(record.identifier, record.deviceIdentifier);
|
|
281
|
-
const potentialHostnames = Array.isArray(connectionProperties.potentialHostnames)
|
|
282
|
-
? connectionProperties.potentialHostnames.filter((value): value is string => typeof value === "string")
|
|
283
|
-
: [];
|
|
284
|
-
const hostnameUdid = potentialHostnames
|
|
285
|
-
.map((hostname) => hostname.match(/([0-9A-Fa-f]{8}-[0-9A-Fa-f]{16})\.coredevice\.local/)?.[1])
|
|
286
|
-
.find((value): value is string => Boolean(value));
|
|
287
|
-
const udid = firstString(hardwareProperties.udid, hostnameUdid, record.udid, record.UDID);
|
|
288
|
-
const id = udid ?? devicectlId;
|
|
289
|
-
const name = firstString(deviceProperties.name, record.name, record.deviceName, record.displayName);
|
|
290
|
-
if (!id || !name) return;
|
|
291
|
-
const state = [
|
|
292
|
-
firstString(record.state, record.connectionState, record.availability, connectionProperties.tunnelState),
|
|
293
|
-
firstString(connectionProperties.transportType),
|
|
294
|
-
firstString(connectionProperties.pairingState),
|
|
295
|
-
]
|
|
296
|
-
.filter(Boolean)
|
|
297
|
-
.join(" ");
|
|
298
|
-
targets.set(id, { id, name, kind: "device", state, devicectlId, xcodebuildId: udid ?? id });
|
|
299
|
-
});
|
|
300
|
-
return dedupeIosRunTargets([...targets.values()]);
|
|
301
|
-
} catch {
|
|
302
|
-
const targets: IosRunTarget[] = [];
|
|
303
|
-
for (const line of output.split(/\r?\n/)) {
|
|
304
|
-
const id = line.match(/[0-9A-Fa-f]{8}-[0-9A-Fa-f]{16}|[0-9A-Fa-f-]{25,}/)?.[0];
|
|
305
|
-
if (!id) continue;
|
|
306
|
-
const name = line.replace(id, "").replace(/[()]/g, " ").trim().replace(/\s+/g, " ") || id;
|
|
307
|
-
targets.push({ id, name, kind: "device" });
|
|
308
|
-
}
|
|
309
|
-
return dedupeIosRunTargets(targets);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
export function parseSimctlDevices(output: string): IosRunTarget[] {
|
|
314
|
-
try {
|
|
315
|
-
const json = JSON.parse(output) as { devices?: Record<string, unknown[]> };
|
|
316
|
-
const devices = json.devices ?? {};
|
|
317
|
-
return Object.entries(devices).flatMap(([runtimeKey, list]) => {
|
|
318
|
-
const runtime = formatSimctlRuntime(runtimeKey);
|
|
319
|
-
return (Array.isArray(list) ? list : []).filter(isRecord).flatMap((device) => {
|
|
320
|
-
const id = firstString(device.udid, device.UDID, device.identifier);
|
|
321
|
-
const name = firstString(device.name, device.displayName);
|
|
322
|
-
const isAvailable = device.isAvailable !== false && device.availabilityError === undefined;
|
|
323
|
-
if (!id || !name || !isAvailable) return [];
|
|
324
|
-
return [
|
|
325
|
-
{
|
|
326
|
-
id,
|
|
327
|
-
name,
|
|
328
|
-
kind: "simulator" as const,
|
|
329
|
-
state: asString(device.state),
|
|
330
|
-
runtime: runtime ?? formatSimctlRuntime(asString(device.runtimeIdentifier)),
|
|
331
|
-
},
|
|
332
|
-
];
|
|
333
|
-
});
|
|
334
|
-
});
|
|
335
|
-
} catch {
|
|
336
|
-
const targets: IosRunTarget[] = [];
|
|
337
|
-
let runtime: string | undefined;
|
|
338
|
-
for (const line of output.split(/\r?\n/)) {
|
|
339
|
-
const header = line.match(/^--\s*(.+?)\s*--\s*$/);
|
|
340
|
-
if (header) {
|
|
341
|
-
runtime = formatSimctlRuntime(header[1]);
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
const match = line.match(/^\s*(.+?)\s+\(([0-9A-Fa-f-]{20,})\)\s+\(([^)]+)\)/);
|
|
345
|
-
if (!match) continue;
|
|
346
|
-
targets.push({ id: match[2], name: match[1].trim(), kind: "simulator", state: match[3], runtime });
|
|
347
|
-
}
|
|
348
|
-
return targets;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
export function buildIosNativeRunCommand({
|
|
353
|
-
appRoot,
|
|
354
|
-
device,
|
|
355
|
-
scheme = "App",
|
|
356
|
-
configuration = "Debug",
|
|
357
|
-
}: {
|
|
358
|
-
appRoot: string;
|
|
359
|
-
device: IosRunTarget;
|
|
360
|
-
scheme?: string;
|
|
361
|
-
configuration?: "Debug" | "Release";
|
|
362
|
-
}): IosNativeRunCommand {
|
|
363
|
-
const derivedDataPath = path.join(appRoot, "ios/DerivedData", device.id);
|
|
364
|
-
const productPlatform = device.kind === "device" ? "iphoneos" : "iphonesimulator";
|
|
365
|
-
const destination =
|
|
366
|
-
device.kind === "device" ? `id=${device.xcodebuildId ?? device.id}` : `platform=iOS Simulator,id=${device.id}`;
|
|
367
|
-
return {
|
|
368
|
-
configuration,
|
|
369
|
-
derivedDataPath,
|
|
370
|
-
appPath: path.join(derivedDataPath, "Build/Products", `${configuration}-${productPlatform}`, "App.app"),
|
|
371
|
-
xcodebuildArgs: [
|
|
372
|
-
"-project",
|
|
373
|
-
"App.xcodeproj",
|
|
374
|
-
"-scheme",
|
|
375
|
-
scheme,
|
|
376
|
-
"-configuration",
|
|
377
|
-
configuration,
|
|
378
|
-
"-destination",
|
|
379
|
-
destination,
|
|
380
|
-
"-derivedDataPath",
|
|
381
|
-
derivedDataPath,
|
|
382
|
-
"build",
|
|
383
|
-
],
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
export function classifyIosRunFailure(log: string): IosRunFailureClassification {
|
|
388
|
-
const lower = log.toLowerCase();
|
|
389
|
-
if (
|
|
390
|
-
lower.includes("swiftuicore") &&
|
|
391
|
-
(lower.includes("library not loaded") || lower.includes("library missing") || lower.includes("dyld"))
|
|
392
|
-
) {
|
|
393
|
-
return {
|
|
394
|
-
kind: "simulator-runtime",
|
|
395
|
-
title: "App crashed loading SwiftUICore — the iOS runtime is too old.",
|
|
396
|
-
detail: `The build links SwiftUICore, which only exists on iOS ${SWIFTUICORE_MIN_IOS_MAJOR}+. Rerun on an iOS ${SWIFTUICORE_MIN_IOS_MAJOR} or newer simulator/device (pass --device to pick one non-interactively).`,
|
|
397
|
-
};
|
|
398
|
-
}
|
|
399
|
-
if (lower.includes("unknown argument: '-index-store-path'") || lower.includes("compiler was not recognized")) {
|
|
400
|
-
return {
|
|
401
|
-
kind: "compiler-toolchain",
|
|
402
|
-
title: "iOS build is using a non-Xcode compiler from the shell environment.",
|
|
403
|
-
detail:
|
|
404
|
-
"Akan removes common Conda/compiler environment variables for native iOS runs. If this persists, run outside the activated toolchain environment.",
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
if (lower.includes("developer mode") && lower.includes("disabled")) {
|
|
408
|
-
return {
|
|
409
|
-
kind: "device-state",
|
|
410
|
-
title: "iOS device Developer Mode is disabled.",
|
|
411
|
-
detail: "Enable Developer Mode on the iPhone, then reconnect and run the command again.",
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
if (lower.includes("untrusted") || lower.includes("not paired") || lower.includes("locked")) {
|
|
415
|
-
return {
|
|
416
|
-
kind: "device-state",
|
|
417
|
-
title: "iOS device is not ready for installation.",
|
|
418
|
-
detail: "Unlock the iPhone, trust this computer, and make sure the device is paired before retrying.",
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
if (
|
|
422
|
-
lower.includes('unable to find utility "devicectl"') ||
|
|
423
|
-
(lower.includes("devicectl") && lower.includes("not found"))
|
|
424
|
-
) {
|
|
425
|
-
return {
|
|
426
|
-
kind: "devicectl-unavailable",
|
|
427
|
-
title: "Xcode devicectl is not available.",
|
|
428
|
-
detail: "Install a recent Xcode version and verify xcode-select points to that Xcode installation.",
|
|
429
|
-
};
|
|
430
|
-
}
|
|
431
|
-
if (
|
|
432
|
-
lower.includes("there are no accounts registered with xcode") ||
|
|
433
|
-
lower.includes("unable to log in with account")
|
|
434
|
-
) {
|
|
435
|
-
return {
|
|
436
|
-
kind: "apple-account",
|
|
437
|
-
title: "Xcode Apple ID is not available.",
|
|
438
|
-
detail: "Sign in to an Apple ID in Xcode Settings > Accounts, then retry the Akan iOS command.",
|
|
439
|
-
};
|
|
440
|
-
}
|
|
441
|
-
if (
|
|
442
|
-
lower.includes("failed registering bundle identifier") ||
|
|
443
|
-
lower.includes("cannot be registered to your development team")
|
|
444
|
-
) {
|
|
445
|
-
return {
|
|
446
|
-
kind: "bundle-identifier",
|
|
447
|
-
title: "iOS bundle identifier is not available for this Apple Developer Team.",
|
|
448
|
-
detail:
|
|
449
|
-
"Change mobile appId to a globally unique bundle identifier that your team can register, then rerun the iOS command.",
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
if (lower.includes("does not have permission") || (lower.includes("your account") && lower.includes("permission"))) {
|
|
453
|
-
return {
|
|
454
|
-
kind: "team-permission",
|
|
455
|
-
title: "Apple Developer Team permission is missing.",
|
|
456
|
-
detail: "Check that the signed-in Apple ID has permission for the selected DEVELOPMENT_TEAM.",
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
if (lower.includes("license agreement") || lower.includes("program license agreement")) {
|
|
460
|
-
return {
|
|
461
|
-
kind: "license-agreement",
|
|
462
|
-
title: "Apple Developer Program license agreement is not accepted.",
|
|
463
|
-
detail: "Accept the latest Apple Developer Program license agreement, then retry.",
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
if (lower.includes("no signing certificate") || lower.includes("doesn't include signing certificate")) {
|
|
467
|
-
return {
|
|
468
|
-
kind: "certificate",
|
|
469
|
-
title: "iOS development signing certificate is missing.",
|
|
470
|
-
detail: "Create or download an Apple Development certificate for the selected team.",
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
if (lower.includes("device") && lower.includes("not") && lower.includes("registered")) {
|
|
474
|
-
return {
|
|
475
|
-
kind: "device-registration",
|
|
476
|
-
title: "iPhone is not registered in the provisioning profile.",
|
|
477
|
-
detail: "Allow provisioning updates with a team that can register this device, or register the device manually.",
|
|
478
|
-
};
|
|
479
|
-
}
|
|
480
|
-
if (lower.includes("no profiles for") || lower.includes("requires a provisioning profile")) {
|
|
481
|
-
return {
|
|
482
|
-
kind: "provisioning-profile",
|
|
483
|
-
title: "Matching iOS provisioning profile was not found.",
|
|
484
|
-
detail:
|
|
485
|
-
"Akan can request Xcode provisioning updates for physical devices, but the Apple account and team must be valid.",
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
return {
|
|
489
|
-
kind: "unknown",
|
|
490
|
-
title: "iOS native run failed.",
|
|
491
|
-
detail:
|
|
492
|
-
"Review the xcodebuild/devicectl output above. You can retry with --noAllowProvisioningUpdates to use the conservative path.",
|
|
493
|
-
};
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
export function formatIosRunFailureMessage(input: {
|
|
497
|
-
classification: IosRunFailureClassification;
|
|
498
|
-
appId: string;
|
|
499
|
-
targetName: string;
|
|
500
|
-
teamId?: string;
|
|
501
|
-
}) {
|
|
502
|
-
return [
|
|
503
|
-
input.classification.title,
|
|
504
|
-
input.classification.detail,
|
|
505
|
-
`Mobile target: ${input.targetName}`,
|
|
506
|
-
`Bundle ID: ${input.appId}`,
|
|
507
|
-
input.teamId ? `Development Team: ${input.teamId}` : null,
|
|
508
|
-
"Capacitor is still used for native project generation and sync; Akan only runs the native build/install step directly.",
|
|
509
|
-
]
|
|
510
|
-
.filter((line): line is string => Boolean(line))
|
|
511
|
-
.join("\n");
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
export function sanitizeIosNativeRunEnv(env: MobileCommandEnv): MobileCommandEnv {
|
|
515
|
-
return Object.fromEntries(Object.entries(env).filter(([key]) => !iosNativeBlockedEnvKeys.has(key)));
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Bundle IDs that ship as scaffold placeholders (or use an obviously generic org segment) and are
|
|
519
|
-
// almost always already claimed on Apple's developer portal, so device signing fails with
|
|
520
|
-
// "cannot be registered to your development team". A unique reverse-DNS id fixes it.
|
|
521
|
-
export const PLACEHOLDER_APP_IDS = [
|
|
522
|
-
"com.myapp.app",
|
|
523
|
-
"com.myorg.myapp",
|
|
524
|
-
"com.example.app",
|
|
525
|
-
"com.example.myapp",
|
|
526
|
-
] as const;
|
|
527
|
-
const placeholderAppIdSegment = /^(example|examples|myorg|myapp|mycompany|myorganization|changeme|todo|sample|test)$/;
|
|
528
|
-
|
|
529
|
-
export const isPlaceholderAppId = (appId: string | null | undefined): boolean => {
|
|
530
|
-
const normalized = appId?.trim().toLowerCase() ?? "";
|
|
531
|
-
if (!normalized) return true;
|
|
532
|
-
if ((PLACEHOLDER_APP_IDS as readonly string[]).includes(normalized)) return true;
|
|
533
|
-
return normalized.split(".").some((segment) => placeholderAppIdSegment.test(segment));
|
|
534
|
-
};
|
|
535
|
-
|
|
536
|
-
const androidReleaseSigningKeys = [
|
|
537
|
-
"MYAPP_RELEASE_STORE_FILE",
|
|
538
|
-
"MYAPP_RELEASE_STORE_PASSWORD",
|
|
539
|
-
"MYAPP_RELEASE_KEY_ALIAS",
|
|
540
|
-
"MYAPP_RELEASE_KEY_PASSWORD",
|
|
541
|
-
] as const;
|
|
542
|
-
|
|
543
|
-
export function getMissingAndroidReleaseSigningKeys({
|
|
544
|
-
env = process.env,
|
|
545
|
-
gradleProperties = "",
|
|
546
|
-
}: {
|
|
547
|
-
env?: NodeJS.ProcessEnv;
|
|
548
|
-
gradleProperties?: string;
|
|
549
|
-
} = {}) {
|
|
550
|
-
return androidReleaseSigningKeys.filter((key) => {
|
|
551
|
-
const gradleEnvKey = `ORG_GRADLE_PROJECT_${key}`;
|
|
552
|
-
return (
|
|
553
|
-
env[key] === undefined &&
|
|
554
|
-
env[gradleEnvKey] === undefined &&
|
|
555
|
-
!new RegExp(`^\\s*${key}\\s*=`, "m").test(gradleProperties)
|
|
556
|
-
);
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
export function formatAndroidReleaseSigningError(missingKeys: readonly string[]) {
|
|
561
|
-
return [
|
|
562
|
-
"Android release signing configuration is incomplete.",
|
|
563
|
-
`Missing: ${missingKeys.join(", ")}`,
|
|
564
|
-
"Set these values in android/gradle.properties or ORG_GRADLE_PROJECT_* environment variables before building a release artifact.",
|
|
565
|
-
].join("\n");
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
export function getAdbDeviceStateIssues(output: string) {
|
|
569
|
-
return output
|
|
570
|
-
.split(/\r?\n/)
|
|
571
|
-
.map((line) => line.trim().split(/\s+/))
|
|
572
|
-
.filter(([id, state]) => id && state && id !== "List")
|
|
573
|
-
.flatMap(([id, state]) => {
|
|
574
|
-
if (state === "unauthorized")
|
|
575
|
-
return [`Android device ${id} is unauthorized. Confirm USB debugging authorization on the device.`];
|
|
576
|
-
if (state === "offline") return [`Android device ${id} is offline. Reconnect the device or restart adb.`];
|
|
577
|
-
return [];
|
|
578
|
-
});
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
export function getAndroidLocalServerHost(adbDevicesOutput: string | undefined, fallbackHost: string) {
|
|
582
|
-
const onlineDeviceIds =
|
|
583
|
-
adbDevicesOutput
|
|
584
|
-
?.split(/\r?\n/)
|
|
585
|
-
.map((line) => line.trim().split(/\s+/))
|
|
586
|
-
.filter(([id, state]) => id && state === "device")
|
|
587
|
-
.map(([id]) => id) ?? [];
|
|
588
|
-
return onlineDeviceIds.length === 1 && onlineDeviceIds[0]?.startsWith("emulator-") ? "10.0.2.2" : fallbackHost;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
export const ANDROID_MIN_SDK_VERSION = 26;
|
|
592
|
-
export function raiseGradleMinSdkVersion(content: string, floor: number = ANDROID_MIN_SDK_VERSION): string | null {
|
|
593
|
-
const match = content.match(/minSdkVersion\s*=\s*(\d+)/);
|
|
594
|
-
if (!match?.[1]) return null;
|
|
595
|
-
const current = Number.parseInt(match[1], 10);
|
|
596
|
-
if (Number.isNaN(current) || current >= floor) return null;
|
|
597
|
-
return content.replace(/(minSdkVersion\s*=\s*)\d+/, `$1${floor}`);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
const mergeAllowNavigation = (configured: unknown, localIp: string | undefined) => {
|
|
601
|
-
const values = Array.isArray(configured)
|
|
602
|
-
? configured.filter((value): value is string => typeof value === "string")
|
|
603
|
-
: [];
|
|
604
|
-
if (localIp) values.push(localIp);
|
|
605
|
-
values.push("localhost");
|
|
606
|
-
return [...new Set(values)];
|
|
607
|
-
};
|
|
608
|
-
|
|
609
|
-
export function assertJsonSerializable(value: unknown, label = "capacitor.config", seen = new WeakSet<object>()) {
|
|
610
|
-
if (value === null) return;
|
|
611
|
-
const valueType = typeof value;
|
|
612
|
-
if (valueType === "function" || valueType === "symbol" || valueType === "bigint" || valueType === "undefined") {
|
|
613
|
-
throw new Error(`${label} must be JSON serializable. Found ${valueType}.`);
|
|
614
|
-
}
|
|
615
|
-
if (valueType === "number" && !Number.isFinite(value)) {
|
|
616
|
-
throw new Error(`${label} must be JSON serializable. Found non-finite number.`);
|
|
617
|
-
}
|
|
618
|
-
if (valueType !== "object") return;
|
|
619
|
-
const objectValue = value as object;
|
|
620
|
-
if (seen.has(objectValue)) throw new Error(`${label} must be JSON serializable. Found circular reference.`);
|
|
621
|
-
seen.add(objectValue);
|
|
622
|
-
if (Array.isArray(value)) {
|
|
623
|
-
value.forEach((item, index) => {
|
|
624
|
-
assertJsonSerializable(item, `${label}[${index}]`, seen);
|
|
625
|
-
});
|
|
626
|
-
return;
|
|
627
|
-
}
|
|
628
|
-
for (const [key, item] of Object.entries(value as Record<string, unknown>)) {
|
|
629
|
-
assertJsonSerializable(item, `${label}.${key}`, seen);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
export function materializeCapacitorConfig(
|
|
634
|
-
target: AkanMobileTargetConfig,
|
|
635
|
-
{ operation, localServerUrl, localIp }: MaterializeCapacitorConfigOptions,
|
|
636
|
-
): CapacitorConfig {
|
|
637
|
-
const {
|
|
638
|
-
name,
|
|
639
|
-
basePath: _basePath,
|
|
640
|
-
indexPath: _indexPath,
|
|
641
|
-
version: _version,
|
|
642
|
-
buildNum: _buildNum,
|
|
643
|
-
assets: _assets,
|
|
644
|
-
permissions: _permissions,
|
|
645
|
-
deepLinks: _deepLinks,
|
|
646
|
-
files: _files,
|
|
647
|
-
appId,
|
|
648
|
-
appName,
|
|
649
|
-
webDir: _webDir,
|
|
650
|
-
plugins,
|
|
651
|
-
server,
|
|
652
|
-
android,
|
|
653
|
-
ios,
|
|
654
|
-
cordova,
|
|
655
|
-
experimental,
|
|
656
|
-
...capacitorConfig
|
|
657
|
-
} = target;
|
|
658
|
-
const serverConfig = isRecord(server) ? server : undefined;
|
|
659
|
-
const cordovaConfig = isRecord(cordova) ? cordova : undefined;
|
|
660
|
-
const experimentalConfig = isRecord(experimental) ? experimental : undefined;
|
|
661
|
-
const pluginsConfig = isRecord(plugins) ? plugins : {};
|
|
662
|
-
const keyboardPluginConfig = isRecord(pluginsConfig.Keyboard) ? pluginsConfig.Keyboard : {};
|
|
663
|
-
const pushNotificationsPluginConfig = isRecord(pluginsConfig.PushNotifications)
|
|
664
|
-
? pluginsConfig.PushNotifications
|
|
665
|
-
: {};
|
|
666
|
-
const usesPushNotifications = target.permissions?.includes("push") ?? false;
|
|
667
|
-
const config: CapacitorConfig = {
|
|
668
|
-
...capacitorConfig,
|
|
669
|
-
appId,
|
|
670
|
-
appName,
|
|
671
|
-
webDir: path.posix.join(".akan", "mobile", name, "www"),
|
|
672
|
-
plugins: {
|
|
673
|
-
CapacitorCookies: { enabled: true },
|
|
674
|
-
...pluginsConfig,
|
|
675
|
-
...(usesPushNotifications || isRecord(pluginsConfig.PushNotifications)
|
|
676
|
-
? {
|
|
677
|
-
PushNotifications: {
|
|
678
|
-
...(usesPushNotifications ? { presentationOptions: ["badge", "sound", "alert"] } : {}),
|
|
679
|
-
...pushNotificationsPluginConfig,
|
|
680
|
-
},
|
|
681
|
-
}
|
|
682
|
-
: {}),
|
|
683
|
-
Keyboard: {
|
|
684
|
-
resize: "none",
|
|
685
|
-
...keyboardPluginConfig,
|
|
686
|
-
},
|
|
687
|
-
},
|
|
688
|
-
android: {
|
|
689
|
-
...(isRecord(android) ? android : {}),
|
|
690
|
-
path: "android",
|
|
691
|
-
},
|
|
692
|
-
ios: {
|
|
693
|
-
...(isRecord(ios) ? ios : {}),
|
|
694
|
-
path: "ios",
|
|
695
|
-
},
|
|
696
|
-
};
|
|
697
|
-
if (operation === "local") {
|
|
698
|
-
if (!localServerUrl) throw new Error(`Local server URL is required for mobile target '${name}'.`);
|
|
699
|
-
config.server = {
|
|
700
|
-
...serverConfig,
|
|
701
|
-
androidScheme: "http",
|
|
702
|
-
url: localServerUrl,
|
|
703
|
-
cleartext: true,
|
|
704
|
-
allowNavigation: mergeAllowNavigation(serverConfig?.allowNavigation, localIp),
|
|
705
|
-
};
|
|
706
|
-
} else if (serverConfig && Object.keys(serverConfig).length > 0) {
|
|
707
|
-
config.server = serverConfig as never;
|
|
708
|
-
}
|
|
709
|
-
if (cordovaConfig && Object.keys(cordovaConfig).length > 0) {
|
|
710
|
-
config.cordova = cordovaConfig as never;
|
|
711
|
-
}
|
|
712
|
-
if (experimentalConfig && Object.keys(experimentalConfig).length > 0) {
|
|
713
|
-
config.experimental = experimentalConfig as never;
|
|
714
|
-
}
|
|
715
|
-
assertJsonSerializable(config);
|
|
716
|
-
return config;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
export class CapacitorApp {
|
|
720
|
-
project: MobileProject & { ios: IosProject; android: AndroidProject };
|
|
721
|
-
iosTargetName = "App";
|
|
722
|
-
readonly targetRoot: string;
|
|
723
|
-
readonly targetRootPath: string;
|
|
724
|
-
readonly targetWebRoot: string;
|
|
725
|
-
readonly targetAssetRoot: string;
|
|
726
|
-
readonly iosRootPath = "ios";
|
|
727
|
-
readonly iosProjectPath = "ios/App";
|
|
728
|
-
readonly androidRootPath = "android";
|
|
729
|
-
readonly androidAssetsPath = "android/app/src/main/assets";
|
|
730
|
-
//* Accumulates iOS entitlements contributed by deep links and plugins during a prepare,
|
|
731
|
-
//* then flushed to App/App.entitlements once (see #flushIosEntitlements).
|
|
732
|
-
#iosEntitlements: Record<string, string | string[]> = {};
|
|
733
|
-
constructor(
|
|
734
|
-
private readonly app: AppExecutor,
|
|
735
|
-
readonly target: AkanMobileTargetConfig,
|
|
736
|
-
) {
|
|
737
|
-
this.targetRootPath = path.posix.join(".akan", "mobile", this.target.name);
|
|
738
|
-
this.targetRoot = path.join(this.app.cwdPath, this.targetRootPath);
|
|
739
|
-
this.targetWebRoot = path.join(this.targetRoot, "www");
|
|
740
|
-
this.targetAssetRoot = path.join(this.targetRoot, "assets");
|
|
741
|
-
this.project = new MobileProject(this.app.cwdPath, {
|
|
742
|
-
android: { path: this.androidRootPath },
|
|
743
|
-
ios: { path: this.iosProjectPath },
|
|
744
|
-
}) as MobileProject & { ios: IosProject; android: AndroidProject };
|
|
745
|
-
}
|
|
746
|
-
async init({
|
|
747
|
-
platform,
|
|
748
|
-
operation = "release",
|
|
749
|
-
env = "debug",
|
|
750
|
-
regenerate = false,
|
|
751
|
-
}: { platform?: "ios" | "android" } & Partial<PrepareConfig> = {}) {
|
|
752
|
-
await mkdir(this.targetRoot, { recursive: true });
|
|
753
|
-
if (regenerate) {
|
|
754
|
-
if (!platform || platform === "ios")
|
|
755
|
-
await rm(path.join(this.app.cwdPath, this.iosRootPath), { recursive: true, force: true });
|
|
756
|
-
if (!platform || platform === "android")
|
|
757
|
-
await rm(path.join(this.app.cwdPath, this.androidRootPath), { recursive: true, force: true });
|
|
758
|
-
}
|
|
759
|
-
const project = this.project as MobileProject;
|
|
760
|
-
await this.project.load();
|
|
761
|
-
if ((!platform || platform === "android") && !project.android) {
|
|
762
|
-
await this.#spawnMobile("npx", ["cap", "add", "android"], { operation, env });
|
|
763
|
-
await this.project.load();
|
|
764
|
-
}
|
|
765
|
-
if ((!platform || platform === "ios") && !project.ios) {
|
|
766
|
-
await this.#spawnMobile("npx", ["cap", "add", "ios"], { operation, env });
|
|
767
|
-
await this.project.load();
|
|
768
|
-
}
|
|
769
|
-
return this;
|
|
770
|
-
}
|
|
771
|
-
async save() {
|
|
772
|
-
await this.project.commit();
|
|
773
|
-
}
|
|
774
|
-
async #prepareIos({ operation, env, regenerate = false, iosRunTargetKind }: PrepareConfig) {
|
|
775
|
-
await this.init({ platform: "ios", operation, env, regenerate });
|
|
776
|
-
await this.#prepareTargetAssets();
|
|
777
|
-
await this.#prepareExternalFiles("ios");
|
|
778
|
-
await this.#applyIosMetadata();
|
|
779
|
-
await this.#applyPermissions({ operation, env });
|
|
780
|
-
await this.#applyDeepLinks("ios", { operation, env });
|
|
781
|
-
await this.#flushIosEntitlements();
|
|
782
|
-
await this.project.commit();
|
|
783
|
-
await this.#setCodeSignEntitlementsInIosIfExists("App/App.entitlements");
|
|
784
|
-
await this.#generateAssets({ operation, env });
|
|
785
|
-
this.app.verbose(`syncing iOS`);
|
|
786
|
-
await this.#spawnMobile("npx", ["cap", "sync", "ios"], { operation, env }, { iosRunTargetKind });
|
|
787
|
-
this.app.verbose(`sync completed.`);
|
|
788
|
-
}
|
|
789
|
-
async buildIos({ env = "debug", regenerate = false }: { env?: RunConfig["env"]; regenerate?: boolean } = {}) {
|
|
790
|
-
await this.prepareWww();
|
|
791
|
-
await this.#prepareIos({ operation: "release", env, regenerate });
|
|
792
|
-
await this.#spawnMobile("npx", ["cap", "build", "ios"], { operation: "release", env }, { stdio: "inherit" });
|
|
793
|
-
this.app.verbose(`build completed iOS.`);
|
|
794
|
-
return;
|
|
795
|
-
}
|
|
796
|
-
async syncIos() {
|
|
797
|
-
await this.#spawnMobile("npx", ["cap", "sync", "ios"], { operation: "local", env: "local" });
|
|
798
|
-
}
|
|
799
|
-
async openIos() {
|
|
800
|
-
await this.#spawnMobile("npx", ["cap", "open", "ios"], { operation: "local", env: "local" });
|
|
801
|
-
}
|
|
802
|
-
async runIos({ operation, env, regenerate = false, noAllowProvisioningUpdates = false, iosDeviceId }: RunIosConfig) {
|
|
803
|
-
if (operation === "release") await this.prepareWww();
|
|
804
|
-
const runTarget = await this.#selectIosRunTarget(iosDeviceId);
|
|
805
|
-
await this.#prepareIos({ operation, env, regenerate, iosRunTargetKind: runTarget.kind });
|
|
806
|
-
if (runTarget.kind === "simulator") {
|
|
807
|
-
await this.#spawnMobile(
|
|
808
|
-
"npx",
|
|
809
|
-
["cap", "run", "ios", "--target", runTarget.id, "--no-sync"],
|
|
810
|
-
{ operation, env },
|
|
811
|
-
{ stdio: "inherit", platform: "ios", iosRunTargetKind: runTarget.kind },
|
|
812
|
-
);
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
await this.#runIosPhysicalDevice({ operation, env, runTarget, noAllowProvisioningUpdates });
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
async #selectIosRunTarget(deviceId?: string) {
|
|
819
|
-
const targets = sortIosRunTargets(await this.#loadIosRunTargets());
|
|
820
|
-
if (deviceId) {
|
|
821
|
-
const needle = deviceId.toLowerCase();
|
|
822
|
-
const found =
|
|
823
|
-
targets.find((target) => target.id === deviceId) ??
|
|
824
|
-
targets.find((target) => target.name.toLowerCase() === needle) ??
|
|
825
|
-
targets.find(
|
|
826
|
-
(target) =>
|
|
827
|
-
target.name.toLowerCase().includes(needle) ||
|
|
828
|
-
target.id.toLowerCase().includes(needle) ||
|
|
829
|
-
(target.runtime?.toLowerCase().includes(needle) ?? false),
|
|
830
|
-
);
|
|
831
|
-
if (!found) {
|
|
832
|
-
const available = targets.map((t) => `${t.name}${t.runtime ? ` (${t.runtime})` : ""}`).join(", ") || "none";
|
|
833
|
-
throw new Error(`iOS run target '${deviceId}' was not found. Available: ${available}`);
|
|
834
|
-
}
|
|
835
|
-
this.#warnIfLegacySimulatorRuntime(found);
|
|
836
|
-
return found;
|
|
837
|
-
}
|
|
838
|
-
if (targets.length === 0) {
|
|
839
|
-
throw new Error("No iOS run targets found. Open Simulator or connect an iPhone, then retry.");
|
|
840
|
-
}
|
|
841
|
-
const selected = await select<IosRunTarget>({
|
|
842
|
-
message: "Select iOS run target",
|
|
843
|
-
choices: targets.map((target) => ({
|
|
844
|
-
name: `[${target.kind}] ${target.name}${target.runtime ? ` — ${target.runtime}` : ""}${target.state ? ` (${target.state})` : ""}`,
|
|
845
|
-
value: target,
|
|
846
|
-
})),
|
|
847
|
-
});
|
|
848
|
-
this.#warnIfLegacySimulatorRuntime(selected);
|
|
849
|
-
return selected;
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
#warnIfLegacySimulatorRuntime(target: IosRunTarget) {
|
|
853
|
-
if (target.kind !== "simulator") return;
|
|
854
|
-
const major = parseIosRuntimeMajor(target.runtime);
|
|
855
|
-
if (major === undefined || major >= SWIFTUICORE_MIN_IOS_MAJOR) return;
|
|
856
|
-
this.app.logger.warn(
|
|
857
|
-
`Selected simulator runs ${target.runtime ?? "an older iOS"}. Recent SDK builds link SwiftUICore and require iOS ${SWIFTUICORE_MIN_IOS_MAJOR}+; if the app crashes at launch with a "Library not loaded: SwiftUICore" dyld error, pick an iOS ${SWIFTUICORE_MIN_IOS_MAJOR}+ simulator instead.`,
|
|
858
|
-
);
|
|
859
|
-
}
|
|
860
|
-
|
|
861
|
-
async #loadIosRunTargets() {
|
|
862
|
-
const devices = await this.#loadPhysicalIosDevices();
|
|
863
|
-
const simulators = await this.#loadIosSimulators();
|
|
864
|
-
return [...devices, ...simulators];
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
async #loadPhysicalIosDevices() {
|
|
868
|
-
try {
|
|
869
|
-
return parseDevicectlDevices(await this.#spawn("xcrun", ["devicectl", "list", "devices", "--json-output", "-"]));
|
|
870
|
-
} catch (jsonError) {
|
|
871
|
-
try {
|
|
872
|
-
return parseDevicectlDevices(await this.#spawn("xcrun", ["devicectl", "list", "devices"]));
|
|
873
|
-
} catch (textError) {
|
|
874
|
-
const classification = classifyIosRunFailure(
|
|
875
|
-
`${jsonError instanceof Error ? jsonError.message : ""}\n${textError instanceof Error ? textError.message : ""}`,
|
|
876
|
-
);
|
|
877
|
-
if (classification.kind === "devicectl-unavailable") this.app.logger.warn(classification.detail);
|
|
878
|
-
return [];
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
async #loadIosSimulators() {
|
|
884
|
-
try {
|
|
885
|
-
return parseSimctlDevices(await this.#spawn("xcrun", ["simctl", "list", "devices", "available", "--json"]));
|
|
886
|
-
} catch {
|
|
887
|
-
try {
|
|
888
|
-
return parseSimctlDevices(await this.#spawn("xcrun", ["simctl", "list", "devices", "available"]));
|
|
889
|
-
} catch {
|
|
890
|
-
return [];
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
async #runIosPhysicalDevice({
|
|
896
|
-
operation,
|
|
897
|
-
env,
|
|
898
|
-
runTarget,
|
|
899
|
-
noAllowProvisioningUpdates,
|
|
900
|
-
}: Pick<RunConfig, "operation" | "env"> & {
|
|
901
|
-
runTarget: IosRunTarget;
|
|
902
|
-
noAllowProvisioningUpdates: boolean;
|
|
903
|
-
}) {
|
|
904
|
-
const mobileEnv = sanitizeIosNativeRunEnv(await this.#commandEnv(operation, env));
|
|
905
|
-
const configContent = await this.#writeCapacitorConfig(
|
|
906
|
-
{ operation, platform: "ios", iosRunTargetKind: runTarget.kind },
|
|
907
|
-
mobileEnv,
|
|
908
|
-
);
|
|
909
|
-
await this.#writeRootCapacitorConfig(configContent);
|
|
910
|
-
const scheme = this.#iosScheme();
|
|
911
|
-
const command = buildIosNativeRunCommand({
|
|
912
|
-
appRoot: this.app.cwdPath,
|
|
913
|
-
device: runTarget,
|
|
914
|
-
scheme,
|
|
915
|
-
configuration: operation === "release" ? "Release" : "Debug",
|
|
916
|
-
});
|
|
917
|
-
const xcodebuildArgs = noAllowProvisioningUpdates
|
|
918
|
-
? command.xcodebuildArgs
|
|
919
|
-
: [...command.xcodebuildArgs.slice(0, -1), "-allowProvisioningUpdates", ...command.xcodebuildArgs.slice(-1)];
|
|
920
|
-
try {
|
|
921
|
-
await this.#spawn("xcodebuild", xcodebuildArgs, {
|
|
922
|
-
cwd: path.join(this.app.cwdPath, this.iosProjectPath),
|
|
923
|
-
env: mobileEnv,
|
|
924
|
-
});
|
|
925
|
-
const devicectlId = runTarget.devicectlId ?? runTarget.id;
|
|
926
|
-
await this.#spawn("xcrun", ["devicectl", "device", "install", "app", "--device", devicectlId, command.appPath], {
|
|
927
|
-
env: mobileEnv,
|
|
928
|
-
});
|
|
929
|
-
await this.#spawn(
|
|
930
|
-
"xcrun",
|
|
931
|
-
["devicectl", "device", "process", "launch", "--device", devicectlId, this.target.appId],
|
|
932
|
-
{
|
|
933
|
-
env: mobileEnv,
|
|
934
|
-
},
|
|
935
|
-
);
|
|
936
|
-
} catch (error) {
|
|
937
|
-
throw new Error(
|
|
938
|
-
formatIosRunFailureMessage({
|
|
939
|
-
classification: classifyIosRunFailure(this.#errorOutput(error)),
|
|
940
|
-
appId: this.target.appId,
|
|
941
|
-
targetName: this.target.name,
|
|
942
|
-
teamId: await this.#getIosDevelopmentTeam(),
|
|
943
|
-
}),
|
|
944
|
-
);
|
|
945
|
-
} finally {
|
|
946
|
-
await this.#clearRootCapacitorConfigs();
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
#iosScheme() {
|
|
951
|
-
return isRecord(this.target.ios) && typeof this.target.ios.scheme === "string" ? this.target.ios.scheme : "App";
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
async #getIosDevelopmentTeam() {
|
|
955
|
-
const pbxprojPath = path.join(this.app.cwdPath, this.iosProjectPath, "App.xcodeproj/project.pbxproj");
|
|
956
|
-
if (!(await Bun.file(pbxprojPath).exists())) return undefined;
|
|
957
|
-
return (await Bun.file(pbxprojPath).text()).match(/DEVELOPMENT_TEAM = ([^;]+);/)?.[1]?.trim();
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
#errorOutput(error: unknown) {
|
|
961
|
-
if (error instanceof CommandExecutionError) return `${error.stdout}\n${error.stderr}\n${error.message}`;
|
|
962
|
-
return error instanceof Error ? error.message : String(error);
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
async #prepareAndroid({ operation, env, regenerate = false }: PrepareConfig) {
|
|
966
|
-
await this.init({ platform: "android", operation, env, regenerate });
|
|
967
|
-
await this.#prepareTargetAssets();
|
|
968
|
-
await this.#prepareExternalFiles("android");
|
|
969
|
-
await this.#applyAndroidMetadata();
|
|
970
|
-
await this.#applyAndroidMinSdkVersion();
|
|
971
|
-
await this.#applyPermissions({ operation, env });
|
|
972
|
-
await this.#applyDeepLinks("android", { operation, env });
|
|
973
|
-
await this.#disableNativeKeyboardResizeInAndroid();
|
|
974
|
-
await this.project.commit();
|
|
975
|
-
await this.#generateAssets({ operation, env });
|
|
976
|
-
await this.#ensureAndroidAssetsDir();
|
|
977
|
-
await this.#ensureAndroidDebugKeystore();
|
|
978
|
-
await this.#spawnMobile("npx", ["cap", "sync", "android"], { operation, env });
|
|
979
|
-
await this.#setDeepLinksInAndroid(this.target.deepLinks?.schemes ?? [], this.target.deepLinks?.domains ?? []);
|
|
980
|
-
}
|
|
981
|
-
|
|
982
|
-
async #updateAndroidBuildTypes() {
|
|
983
|
-
//keystore 기본 설정 및 debug, release 설정
|
|
984
|
-
|
|
985
|
-
const appGradle = await FileEditor.create(path.join(this.app.cwdPath, this.androidRootPath, "app/build.gradle"));
|
|
986
|
-
const buildTypesBlock = `
|
|
987
|
-
debug {
|
|
988
|
-
applicationIdSuffix ".debug"
|
|
989
|
-
versionNameSuffix "-DEBUG"
|
|
990
|
-
debuggable true
|
|
991
|
-
minifyEnabled false
|
|
992
|
-
}
|
|
993
|
-
`;
|
|
994
|
-
const singinConfigBlock = `
|
|
995
|
-
signingConfigs {
|
|
996
|
-
debug {
|
|
997
|
-
storeFile file('debug.keystore')
|
|
998
|
-
storePassword 'android'
|
|
999
|
-
keyAlias 'androiddebugkey'
|
|
1000
|
-
keyPassword 'android'
|
|
1001
|
-
}
|
|
1002
|
-
release {
|
|
1003
|
-
if (project.hasProperty('MYAPP_RELEASE_STORE_FILE')) {
|
|
1004
|
-
storeFile file(MYAPP_RELEASE_STORE_FILE)
|
|
1005
|
-
storePassword MYAPP_RELEASE_STORE_PASSWORD
|
|
1006
|
-
keyAlias MYAPP_RELEASE_KEY_ALIAS
|
|
1007
|
-
keyPassword MYAPP_RELEASE_KEY_PASSWORD
|
|
1008
|
-
}
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
`;
|
|
1012
|
-
if (appGradle.find("signingConfigs {") === -1) {
|
|
1013
|
-
appGradle.insertBefore("buildTypes {", singinConfigBlock);
|
|
1014
|
-
}
|
|
1015
|
-
if (appGradle.find(`applicationIdSuffix ".debug"`) === -1) {
|
|
1016
|
-
appGradle.insertAfter("buildTypes {", buildTypesBlock);
|
|
1017
|
-
}
|
|
1018
|
-
await appGradle.save();
|
|
1019
|
-
}
|
|
1020
|
-
async buildAndroid(
|
|
1021
|
-
assembleType: "apk" | "aab",
|
|
1022
|
-
{ env = "debug", regenerate = false }: { env?: RunConfig["env"]; regenerate?: boolean } = {},
|
|
1023
|
-
) {
|
|
1024
|
-
await this.prepareWww();
|
|
1025
|
-
await this.#prepareAndroid({ operation: "release", env, regenerate });
|
|
1026
|
-
await this.#assertAndroidReleaseSigningConfig();
|
|
1027
|
-
await this.#updateAndroidBuildTypes();
|
|
1028
|
-
//윈도우는 gradlew.bat 사용
|
|
1029
|
-
const isWindows = process.platform === "win32";
|
|
1030
|
-
const gradleCommand = isWindows ? "gradlew.bat" : "./gradlew";
|
|
1031
|
-
|
|
1032
|
-
await this.app.spawn(gradleCommand, [assembleType === "apk" ? "assembleRelease" : "bundleRelease"], {
|
|
1033
|
-
stdio: "inherit",
|
|
1034
|
-
cwd: path.join(this.app.cwdPath, this.androidRootPath),
|
|
1035
|
-
env: await this.#commandEnv("release", env),
|
|
1036
|
-
});
|
|
1037
|
-
}
|
|
1038
|
-
async openAndroid() {
|
|
1039
|
-
await this.#spawnMobile("npx", ["cap", "open", "android"], { operation: "local", env: "local" });
|
|
1040
|
-
}
|
|
1041
|
-
async #disableNativeKeyboardResizeInAndroid() {
|
|
1042
|
-
const manifestPath = path.join(this.app.cwdPath, this.androidRootPath, "app/src/main/AndroidManifest.xml");
|
|
1043
|
-
let manifest = await readFile(manifestPath, "utf8");
|
|
1044
|
-
let changed = false;
|
|
1045
|
-
manifest = manifest.replace(/<activity\b[^>]*android:name="\.MainActivity"[^>]*>/, (activityTag) => {
|
|
1046
|
-
if (activityTag.includes("android:windowSoftInputMode=")) {
|
|
1047
|
-
const nextTag = activityTag.replace(
|
|
1048
|
-
/android:windowSoftInputMode="[^"]*"/,
|
|
1049
|
-
'android:windowSoftInputMode="adjustNothing"',
|
|
1050
|
-
);
|
|
1051
|
-
changed ||= nextTag !== activityTag;
|
|
1052
|
-
return nextTag;
|
|
1053
|
-
}
|
|
1054
|
-
changed = true;
|
|
1055
|
-
return activityTag.replace(/>$/, '\n android:windowSoftInputMode="adjustNothing">');
|
|
1056
|
-
});
|
|
1057
|
-
if (changed) await writeFile(manifestPath, manifest);
|
|
1058
|
-
}
|
|
1059
|
-
async #ensureAndroidAssetsDir() {
|
|
1060
|
-
await mkdir(path.join(this.app.cwdPath, this.androidAssetsPath), { recursive: true });
|
|
1061
|
-
}
|
|
1062
|
-
async #ensureAndroidDebugKeystore() {
|
|
1063
|
-
const keystorePath = path.join(this.app.cwdPath, this.androidRootPath, "app/debug.keystore");
|
|
1064
|
-
if (await Bun.file(keystorePath).exists()) return;
|
|
1065
|
-
|
|
1066
|
-
await this.#spawn("keytool", [
|
|
1067
|
-
"-genkeypair",
|
|
1068
|
-
"-v",
|
|
1069
|
-
"-keystore",
|
|
1070
|
-
keystorePath,
|
|
1071
|
-
"-storepass",
|
|
1072
|
-
"android",
|
|
1073
|
-
"-alias",
|
|
1074
|
-
"androiddebugkey",
|
|
1075
|
-
"-keypass",
|
|
1076
|
-
"android",
|
|
1077
|
-
"-keyalg",
|
|
1078
|
-
"RSA",
|
|
1079
|
-
"-keysize",
|
|
1080
|
-
"2048",
|
|
1081
|
-
"-validity",
|
|
1082
|
-
"10000",
|
|
1083
|
-
"-dname",
|
|
1084
|
-
"CN=Android Debug,O=Android,C=US",
|
|
1085
|
-
]);
|
|
1086
|
-
}
|
|
1087
|
-
async syncAndroid(options: { regenerate?: boolean } = {}) {
|
|
1088
|
-
await this.prepareWww();
|
|
1089
|
-
await this.#prepareAndroid({ operation: "release", env: "debug", ...options });
|
|
1090
|
-
this.app.log(`Sync Android Completed.`);
|
|
1091
|
-
}
|
|
1092
|
-
async runAndroid({ operation, env, regenerate = false }: RunConfig) {
|
|
1093
|
-
if (operation === "release") await this.prepareWww();
|
|
1094
|
-
await this.#prepareAndroid({ operation, env, regenerate });
|
|
1095
|
-
await this.#assertAndroidAdbReady();
|
|
1096
|
-
this.app.logger.info(`Running Android in ${operation} mode on ${env} env`);
|
|
1097
|
-
const args = ["cap", "run", "android"];
|
|
1098
|
-
await this.#spawnMobile("npx", args, { operation, env }, { stdio: "inherit" });
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
async #assertAndroidReleaseSigningConfig() {
|
|
1102
|
-
const gradlePropertiesPath = path.join(this.app.cwdPath, this.androidRootPath, "gradle.properties");
|
|
1103
|
-
const gradleProperties = (await Bun.file(gradlePropertiesPath).exists())
|
|
1104
|
-
? await Bun.file(gradlePropertiesPath).text()
|
|
1105
|
-
: "";
|
|
1106
|
-
const missingKeys = getMissingAndroidReleaseSigningKeys({ gradleProperties });
|
|
1107
|
-
if (missingKeys.length > 0) throw new Error(formatAndroidReleaseSigningError(missingKeys));
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
async #assertAndroidAdbReady() {
|
|
1111
|
-
try {
|
|
1112
|
-
const issues = getAdbDeviceStateIssues(await this.#spawn("adb", ["devices"]));
|
|
1113
|
-
if (issues.length > 0) throw new Error(issues.join("\n"));
|
|
1114
|
-
} catch (error) {
|
|
1115
|
-
if (error instanceof CommandExecutionError) return;
|
|
1116
|
-
throw error;
|
|
1117
|
-
}
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
async releaseIos() {
|
|
1121
|
-
await this.prepareWww();
|
|
1122
|
-
await this.#prepareIos({ operation: "release", env: "main" });
|
|
1123
|
-
}
|
|
1124
|
-
async releaseAndroid() {
|
|
1125
|
-
await this.prepareWww();
|
|
1126
|
-
await this.#prepareAndroid({ operation: "release", env: "main" });
|
|
1127
|
-
}
|
|
1128
|
-
async prepareWww() {
|
|
1129
|
-
const htmlSource = path.join(this.app.dist.cwdPath, "csr", targetHtmlFilename(this.target));
|
|
1130
|
-
if (!(await Bun.file(htmlSource).exists()))
|
|
1131
|
-
throw new Error(`CSR html for mobile target '${this.target.name}' not found: ${htmlSource}`);
|
|
1132
|
-
await rm(this.targetWebRoot, { recursive: true, force: true });
|
|
1133
|
-
await mkdir(this.targetWebRoot, { recursive: true });
|
|
1134
|
-
await Bun.write(
|
|
1135
|
-
path.join(this.targetWebRoot, "index.html"),
|
|
1136
|
-
this.#injectMobileTargetMeta(await Bun.file(htmlSource).text()),
|
|
1137
|
-
);
|
|
1138
|
-
}
|
|
1139
|
-
#injectMobileTargetMeta(html: string) {
|
|
1140
|
-
const basePath = this.target.basePath?.replace(/^\/+|\/+$/g, "") ?? "";
|
|
1141
|
-
const script = `<script>window.__AKAN_MOBILE_TARGET__=${JSON.stringify({
|
|
1142
|
-
name: this.target.name,
|
|
1143
|
-
basePath,
|
|
1144
|
-
indexPath: this.target.indexPath,
|
|
1145
|
-
})};</script>`;
|
|
1146
|
-
if (html.includes("window.__AKAN_MOBILE_TARGET__")) return html;
|
|
1147
|
-
return html.replace(/<\/head\s*>/i, `${script}\n</head>`);
|
|
1148
|
-
}
|
|
1149
|
-
async #writeCapacitorConfig(
|
|
1150
|
-
{
|
|
1151
|
-
operation,
|
|
1152
|
-
platform,
|
|
1153
|
-
iosRunTargetKind,
|
|
1154
|
-
}: Pick<RunConfig, "operation"> & { platform?: MobilePlatform; iosRunTargetKind?: IosRunTargetKind },
|
|
1155
|
-
commandEnv: MobileCommandEnv,
|
|
1156
|
-
) {
|
|
1157
|
-
await mkdir(this.targetRoot, { recursive: true });
|
|
1158
|
-
let localIp: string | undefined;
|
|
1159
|
-
if (operation === "local") {
|
|
1160
|
-
const override = commandEnv.AKAN_PUBLIC_CLIENT_HOST ?? process.env.AKAN_PUBLIC_CLIENT_HOST;
|
|
1161
|
-
const resolution = await this.#resolveLocalDevHost({ override, platform, iosRunTargetKind });
|
|
1162
|
-
localIp = resolution.host;
|
|
1163
|
-
this.#logDevHostResolution(resolution, commandEnv);
|
|
1164
|
-
}
|
|
1165
|
-
const config = materializeCapacitorConfig(this.target, {
|
|
1166
|
-
operation,
|
|
1167
|
-
localIp,
|
|
1168
|
-
localServerUrl: localIp ? this.#localCsrUrl(localIp, commandEnv) : undefined,
|
|
1169
|
-
});
|
|
1170
|
-
const content = `${JSON.stringify(config, null, 2)}\n`;
|
|
1171
|
-
await Bun.write(path.join(this.targetRoot, "capacitor.config.json"), content);
|
|
1172
|
-
return content;
|
|
1173
|
-
}
|
|
1174
|
-
// An explicit override always wins; an emulator/simulator run reaches the host machine through a
|
|
1175
|
-
// loopback alias (10.0.2.2 / localhost), so LAN detection only applies to physical-device runs.
|
|
1176
|
-
async #resolveLocalDevHost({
|
|
1177
|
-
override,
|
|
1178
|
-
platform,
|
|
1179
|
-
iosRunTargetKind,
|
|
1180
|
-
}: {
|
|
1181
|
-
override?: string;
|
|
1182
|
-
platform?: MobilePlatform;
|
|
1183
|
-
iosRunTargetKind?: IosRunTargetKind;
|
|
1184
|
-
}): Promise<LocalDevHostResolution> {
|
|
1185
|
-
const resolution = selectLocalDevHost(os.networkInterfaces(), { override });
|
|
1186
|
-
if (resolution.source === "override") return resolution;
|
|
1187
|
-
if (platform === "ios" && iosRunTargetKind === "simulator")
|
|
1188
|
-
return { ...resolution, host: "localhost", source: "platform" };
|
|
1189
|
-
if (platform === "android") {
|
|
1190
|
-
try {
|
|
1191
|
-
const host = getAndroidLocalServerHost(await this.#spawn("adb", ["devices"]), resolution.host);
|
|
1192
|
-
return host === resolution.host ? resolution : { ...resolution, host, source: "platform" };
|
|
1193
|
-
} catch {
|
|
1194
|
-
return resolution;
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
return resolution;
|
|
1198
|
-
}
|
|
1199
|
-
// Surface the live-reload URL a physical device must reach, and warn when auto-detection landed on
|
|
1200
|
-
// a likely-unreachable host so a blank WebView is not mistaken for an app bug.
|
|
1201
|
-
#logDevHostResolution(resolution: LocalDevHostResolution, commandEnv: MobileCommandEnv) {
|
|
1202
|
-
this.app.log(`Mobile live-reload server: ${this.#localCsrUrl(resolution.host, commandEnv)}`);
|
|
1203
|
-
if (resolution.source === "override" || resolution.source === "platform") return;
|
|
1204
|
-
const suspicious = resolution.host === "127.0.0.1" || resolution.host.startsWith("169.254.");
|
|
1205
|
-
const alternatives = resolution.candidates.filter((candidate) => candidate.address !== resolution.host);
|
|
1206
|
-
if (!suspicious && alternatives.length === 0) return;
|
|
1207
|
-
const alternativeText = alternatives.length
|
|
1208
|
-
? ` Other interfaces: ${alternatives.map((candidate) => `${candidate.address} (${candidate.name})`).join(", ")}.`
|
|
1209
|
-
: "";
|
|
1210
|
-
this.app.logger.warn(
|
|
1211
|
-
`A physical device must reach ${resolution.host} on your LAN.${suspicious ? " That address looks non-routable." : ""}${alternativeText} If the device shows a blank screen, pin the right one with AKAN_PUBLIC_CLIENT_HOST=<ip>.`,
|
|
1212
|
-
);
|
|
1213
|
-
}
|
|
1214
|
-
async #prepareTargetAssets() {
|
|
1215
|
-
if (!this.target.assets) return;
|
|
1216
|
-
await mkdir(this.targetAssetRoot, { recursive: true });
|
|
1217
|
-
if (this.target.assets.icon)
|
|
1218
|
-
await cp(path.join(this.app.cwdPath, this.target.assets.icon), path.join(this.targetAssetRoot, "icon.png"), {
|
|
1219
|
-
force: true,
|
|
1220
|
-
});
|
|
1221
|
-
if (this.target.assets.splash)
|
|
1222
|
-
await cp(path.join(this.app.cwdPath, this.target.assets.splash), path.join(this.targetAssetRoot, "splash.png"), {
|
|
1223
|
-
force: true,
|
|
1224
|
-
});
|
|
1225
|
-
}
|
|
1226
|
-
async #prepareExternalFiles(platform: "ios" | "android") {
|
|
1227
|
-
const files = this.target.files?.[platform];
|
|
1228
|
-
if (!files) return;
|
|
1229
|
-
const platformRoot = path.join(this.app.cwdPath, platform === "ios" ? this.iosRootPath : this.androidRootPath);
|
|
1230
|
-
await Promise.all(
|
|
1231
|
-
Object.entries(files).map(async ([to, from]) => {
|
|
1232
|
-
const targetPath = path.join(platformRoot, to);
|
|
1233
|
-
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
1234
|
-
await cp(path.join(this.app.cwdPath, from), targetPath, { force: true });
|
|
1235
|
-
}),
|
|
1236
|
-
);
|
|
1237
|
-
}
|
|
1238
|
-
async #generateAssets({ operation, env }: Pick<RunConfig, "operation" | "env">) {
|
|
1239
|
-
if (!this.target.assets) return;
|
|
1240
|
-
await this.#spawnMobile(
|
|
1241
|
-
"npx",
|
|
1242
|
-
[
|
|
1243
|
-
"@capacitor/assets",
|
|
1244
|
-
"generate",
|
|
1245
|
-
"--assetPath",
|
|
1246
|
-
path.posix.join(this.targetRootPath, "assets"),
|
|
1247
|
-
"--iosProject",
|
|
1248
|
-
this.iosProjectPath,
|
|
1249
|
-
"--androidProject",
|
|
1250
|
-
this.androidRootPath,
|
|
1251
|
-
],
|
|
1252
|
-
{ operation, env },
|
|
1253
|
-
);
|
|
1254
|
-
}
|
|
1255
|
-
async #applyIosMetadata() {
|
|
1256
|
-
this.project.ios.setBundleId("App", "Debug", this.target.appId);
|
|
1257
|
-
this.project.ios.setBundleId("App", "Release", this.target.appId);
|
|
1258
|
-
await this.project.ios.setVersion("App", "Debug", this.target.version);
|
|
1259
|
-
await this.project.ios.setVersion("App", "Release", this.target.version);
|
|
1260
|
-
await this.project.ios.setBuild("App", "Debug", this.target.buildNum);
|
|
1261
|
-
await this.project.ios.setBuild("App", "Release", this.target.buildNum);
|
|
1262
|
-
}
|
|
1263
|
-
async #applyAndroidMetadata() {
|
|
1264
|
-
await this.project.android.setVersionName(this.target.version);
|
|
1265
|
-
await this.project.android.setPackageName(this.target.appId);
|
|
1266
|
-
await this.project.android.setVersionCode(this.target.buildNum);
|
|
1267
|
-
await this.project.android.setAppName(this.target.appName);
|
|
1268
|
-
}
|
|
1269
|
-
async #applyAndroidMinSdkVersion() {
|
|
1270
|
-
const variablesGradlePath = path.join(this.app.cwdPath, this.androidRootPath, "variables.gradle");
|
|
1271
|
-
if (!(await Bun.file(variablesGradlePath).exists())) return;
|
|
1272
|
-
const updated = raiseGradleMinSdkVersion(await Bun.file(variablesGradlePath).text());
|
|
1273
|
-
if (!updated) return;
|
|
1274
|
-
await writeFile(variablesGradlePath, updated);
|
|
1275
|
-
this.app.verbose(`Raised Android minSdkVersion to ${ANDROID_MIN_SDK_VERSION} in variables.gradle`);
|
|
1276
|
-
}
|
|
1277
|
-
async #applyPermissions({ operation, env }: Pick<RunConfig, "operation" | "env">) {
|
|
1278
|
-
const plugins = await this.app.collectPlugins();
|
|
1279
|
-
const nativePlugins = new Map<MobilePermission, AkanPlugin[]>();
|
|
1280
|
-
for (const plugin of plugins) {
|
|
1281
|
-
const permission = plugin.capacitor?.permission;
|
|
1282
|
-
if (!permission || !plugin.capacitor?.configureNative) continue;
|
|
1283
|
-
const claimants = nativePlugins.get(permission) ?? [];
|
|
1284
|
-
claimants.push(plugin);
|
|
1285
|
-
nativePlugins.set(permission, claimants);
|
|
1286
|
-
}
|
|
1287
|
-
for (const permission of this.target.permissions ?? []) {
|
|
1288
|
-
const claimants = nativePlugins.get(permission);
|
|
1289
|
-
if (claimants?.length) {
|
|
1290
|
-
const ctx = this.#makeNativeContext({ operation, env });
|
|
1291
|
-
for (const plugin of claimants) await plugin.capacitor?.configureNative?.(ctx);
|
|
1292
|
-
continue;
|
|
1293
|
-
}
|
|
1294
|
-
if (permission === "camera") await this.addCamera();
|
|
1295
|
-
else if (permission === "contacts") await this.addContact();
|
|
1296
|
-
else if (permission === "location") await this.addLocation();
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
#makeNativeContext({ operation, env }: Pick<RunConfig, "operation" | "env">): AkanNativeContext {
|
|
1300
|
-
return {
|
|
1301
|
-
appPath: this.app.cwdPath,
|
|
1302
|
-
executor: this.app,
|
|
1303
|
-
target: this.target,
|
|
1304
|
-
operation,
|
|
1305
|
-
env,
|
|
1306
|
-
setIosUsageDescriptions: (descriptions) => this.#setPermissionInIos(descriptions),
|
|
1307
|
-
updateIosInfoPlist: async (values) => {
|
|
1308
|
-
await Promise.all([
|
|
1309
|
-
this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", values),
|
|
1310
|
-
this.project.ios.updateInfoPlist(this.iosTargetName, "Release", values),
|
|
1311
|
-
]);
|
|
1312
|
-
},
|
|
1313
|
-
addIosEntitlements: (entitlements) => this.#addIosEntitlements(entitlements),
|
|
1314
|
-
editIosAppDelegate: (transform) => this.#editIosAppDelegate(transform),
|
|
1315
|
-
addAndroidPermissions: (permissions) => {
|
|
1316
|
-
this.#setPermissionsInAndroid(permissions);
|
|
1317
|
-
},
|
|
1318
|
-
addAndroidFeatures: (features) => {
|
|
1319
|
-
this.#setFeaturesInAndroid(features);
|
|
1320
|
-
},
|
|
1321
|
-
};
|
|
1322
|
-
}
|
|
1323
|
-
async #applyDeepLinks(platform: "ios" | "android", { operation, env }: Pick<RunConfig, "operation" | "env">) {
|
|
1324
|
-
const deepLinks = this.target.deepLinks;
|
|
1325
|
-
if (!deepLinks) return;
|
|
1326
|
-
const schemes = deepLinks.schemes ?? [];
|
|
1327
|
-
const domains = deepLinks.domains ?? [];
|
|
1328
|
-
if (domains.length > 0) this.#assertDeepLinkVerificationConfig(platform, { operation, env });
|
|
1329
|
-
if (platform === "ios") {
|
|
1330
|
-
if (schemes.length > 0) {
|
|
1331
|
-
await this.#setPermissionInIos({
|
|
1332
|
-
appTransportSecurity: "",
|
|
1333
|
-
});
|
|
1334
|
-
await this.#setUrlSchemesInIos(schemes);
|
|
1335
|
-
}
|
|
1336
|
-
if (domains.length > 0)
|
|
1337
|
-
this.#addIosEntitlements({
|
|
1338
|
-
"com.apple.developer.associated-domains": domains.map((domain) => `applinks:${domain}`),
|
|
1339
|
-
});
|
|
1340
|
-
return;
|
|
1341
|
-
}
|
|
1342
|
-
if (platform === "android") {
|
|
1343
|
-
for (const scheme of schemes) {
|
|
1344
|
-
this.project.android
|
|
1345
|
-
.getAndroidManifest()
|
|
1346
|
-
.injectFragment(
|
|
1347
|
-
"activity",
|
|
1348
|
-
`<intent-filter><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><data android:scheme="${scheme}" /></intent-filter>`,
|
|
1349
|
-
);
|
|
1350
|
-
}
|
|
1351
|
-
for (const domain of domains) {
|
|
1352
|
-
const pathPrefix = resolveMobilePath(this.target, "/");
|
|
1353
|
-
this.project.android
|
|
1354
|
-
.getAndroidManifest()
|
|
1355
|
-
.injectFragment(
|
|
1356
|
-
"activity",
|
|
1357
|
-
`<intent-filter android:autoVerify="true"><action android:name="android.intent.action.VIEW" /><category android:name="android.intent.category.DEFAULT" /><category android:name="android.intent.category.BROWSABLE" /><data android:scheme="https" android:host="${domain}" android:pathPrefix="${pathPrefix}" /></intent-filter>`,
|
|
1358
|
-
);
|
|
1359
|
-
}
|
|
1360
|
-
await this.#setDeepLinksInAndroid(schemes, domains);
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
#assertDeepLinkVerificationConfig(
|
|
1364
|
-
platform: "ios" | "android",
|
|
1365
|
-
{ operation, env }: Pick<RunConfig, "operation" | "env">,
|
|
1366
|
-
) {
|
|
1367
|
-
const deepLinks = this.target.deepLinks;
|
|
1368
|
-
if (!deepLinks?.domains?.length) return;
|
|
1369
|
-
if (platform === "ios" && !deepLinks.ios?.teamId) {
|
|
1370
|
-
throw new Error(
|
|
1371
|
-
`Mobile target '${this.target.name}' uses deepLinks.domains but is missing deepLinks.ios.teamId in apps/${this.app.name}/akan.config.ts`,
|
|
1372
|
-
);
|
|
1373
|
-
}
|
|
1374
|
-
if (platform === "android" && !deepLinks.android?.sha256CertFingerprints?.length) {
|
|
1375
|
-
const message = `Mobile target '${this.target.name}' uses deepLinks.domains but is missing deepLinks.android.sha256CertFingerprints in apps/${this.app.name}/akan.config.ts`;
|
|
1376
|
-
if (operation === "release" || env === "main") throw new Error(message);
|
|
1377
|
-
this.app.logger.warn(message);
|
|
1378
|
-
}
|
|
1379
|
-
}
|
|
1380
|
-
async #commandEnv(operation: "local" | "release", env: "local" | "debug" | "develop" | "main") {
|
|
1381
|
-
const devPort = operation === "local" ? (await this.app.getDevPort()).toString() : undefined;
|
|
1382
|
-
return this.app.getCommandEnv({
|
|
1383
|
-
APP_OPERATION_MODE: operation,
|
|
1384
|
-
AKAN_PUBLIC_OPERATION_MODE: env === "local" ? "local" : "cloud",
|
|
1385
|
-
AKAN_PUBLIC_ENV: env,
|
|
1386
|
-
AKAN_MOBILE_TARGET: this.target.name,
|
|
1387
|
-
...(devPort ? { PORT: devPort, AKAN_PUBLIC_CLIENT_PORT: devPort, AKAN_PUBLIC_SERVER_PORT: devPort } : {}),
|
|
1388
|
-
});
|
|
1389
|
-
}
|
|
1390
|
-
#localCsrUrl(ip: string, commandEnv: MobileCommandEnv) {
|
|
1391
|
-
const basePath = this.target.basePath?.replace(/^\/+|\/+$/g, "");
|
|
1392
|
-
const locale = commandEnv.AKAN_PUBLIC_DEFAULT_LOCALE ?? "en";
|
|
1393
|
-
const pathname = basePath ? `${locale}/${basePath}` : `${locale}/`;
|
|
1394
|
-
const port = commandEnv.AKAN_PUBLIC_CLIENT_PORT ?? commandEnv.PORT ?? "8282";
|
|
1395
|
-
const params = new URLSearchParams({ csr: "true", akanMobileTarget: this.target.name });
|
|
1396
|
-
if (basePath) params.set("akanMobileBasePath", basePath);
|
|
1397
|
-
if (this.target.indexPath) params.set("akanMobileIndexPath", this.target.indexPath);
|
|
1398
|
-
return `http://${ip}:${port}/${pathname}?${params}`;
|
|
1399
|
-
}
|
|
1400
|
-
async #clearRootCapacitorConfigs() {
|
|
1401
|
-
await clearRootCapacitorConfigs(this.app.cwdPath);
|
|
1402
|
-
}
|
|
1403
|
-
async #writeRootCapacitorConfig(content: string) {
|
|
1404
|
-
await writeRootCapacitorConfig(this.app.cwdPath, content);
|
|
1405
|
-
}
|
|
1406
|
-
async #spawn(command: string, args: string[] = [], options: Parameters<AppExecutor["spawn"]>[2] = {}) {
|
|
1407
|
-
return await this.app.spawn(command, args, { cwd: this.app.cwdPath, ...options });
|
|
1408
|
-
}
|
|
1409
|
-
async #spawnMobile(
|
|
1410
|
-
command: string,
|
|
1411
|
-
args: string[] = [],
|
|
1412
|
-
{ operation, env }: Pick<RunConfig, "operation" | "env">,
|
|
1413
|
-
options: SpawnMobileOptions = {},
|
|
1414
|
-
) {
|
|
1415
|
-
const { iosRunTargetKind, platform, ...spawnOptions } = options;
|
|
1416
|
-
const mobileEnv = { ...(await this.#commandEnv(operation, env)), ...options.env };
|
|
1417
|
-
const configContent = await this.#writeCapacitorConfig(
|
|
1418
|
-
{ operation, platform: platform ?? this.#inferMobilePlatform(args), iosRunTargetKind },
|
|
1419
|
-
mobileEnv,
|
|
1420
|
-
);
|
|
1421
|
-
await this.#writeRootCapacitorConfig(configContent);
|
|
1422
|
-
try {
|
|
1423
|
-
return await this.#spawn(command, args, {
|
|
1424
|
-
...spawnOptions,
|
|
1425
|
-
env: mobileEnv,
|
|
1426
|
-
});
|
|
1427
|
-
} finally {
|
|
1428
|
-
await this.#clearRootCapacitorConfigs();
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
#inferMobilePlatform(args: string[]): MobilePlatform | undefined {
|
|
1432
|
-
if (args.includes("android")) return "android";
|
|
1433
|
-
if (args.includes("ios")) return "ios";
|
|
1434
|
-
return undefined;
|
|
1435
|
-
}
|
|
1436
|
-
async addCamera() {
|
|
1437
|
-
await this.#setPermissionInIos({
|
|
1438
|
-
cameraUsageDescription: "$(PRODUCT_NAME) requires access to the camera to take photos.",
|
|
1439
|
-
photoAddUsageDescription: "$(PRODUCT_NAME) requires access to the photo library to take photos.",
|
|
1440
|
-
photoUsageDescription: "$(PRODUCT_NAME) requires access to the photo library to take photos.",
|
|
1441
|
-
});
|
|
1442
|
-
this.#setPermissionsInAndroid(["READ_MEDIA_IMAGES", "READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE"]);
|
|
1443
|
-
}
|
|
1444
|
-
async addContact() {
|
|
1445
|
-
await this.#setPermissionInIos({
|
|
1446
|
-
contactsUsageDescription: "$(PRODUCT_NAME) requires access to the contacts to add new contacts.",
|
|
1447
|
-
});
|
|
1448
|
-
this.#setPermissionsInAndroid(["READ_CONTACTS", "WRITE_CONTACTS"]);
|
|
1449
|
-
}
|
|
1450
|
-
async addLocation() {
|
|
1451
|
-
await this.#setPermissionInIos({
|
|
1452
|
-
locationAlwaysUsageDescription: "$(PRODUCT_NAME) requires access to the location to get the user's location.",
|
|
1453
|
-
locationWhenInUseUsageDescription: "$(PRODUCT_NAME) requires access to the location to get the user's location.",
|
|
1454
|
-
});
|
|
1455
|
-
this.#setPermissionsInAndroid(["ACCESS_COARSE_LOCATION", "ACCESS_FINE_LOCATION"]);
|
|
1456
|
-
this.#setFeaturesInAndroid(["android.hardware.location.gps"]);
|
|
1457
|
-
}
|
|
1458
|
-
#addIosEntitlements(entitlements: Record<string, string | string[]>) {
|
|
1459
|
-
Object.assign(this.#iosEntitlements, entitlements);
|
|
1460
|
-
}
|
|
1461
|
-
//* Generic in-place transform of ios/App/App/AppDelegate.swift (no-op if the file is absent).
|
|
1462
|
-
//* Feature-specific native wiring (e.g. Firebase) lives in plugins, not the framework.
|
|
1463
|
-
async #editIosAppDelegate(transform: (content: string) => string) {
|
|
1464
|
-
const appDelegatePath = path.join(this.app.cwdPath, this.iosProjectPath, "App/AppDelegate.swift");
|
|
1465
|
-
if (!(await Bun.file(appDelegatePath).exists())) return;
|
|
1466
|
-
const editor = await FileEditor.create(appDelegatePath);
|
|
1467
|
-
const content = editor.getContent();
|
|
1468
|
-
const next = transform(content);
|
|
1469
|
-
if (next !== content) await editor.setContent(next).save();
|
|
1470
|
-
}
|
|
1471
|
-
async #setPermissionInIos(permissions: { [key: string]: string }) {
|
|
1472
|
-
const updateNs = Object.fromEntries(
|
|
1473
|
-
Object.entries(permissions).map(([key, value]) => [`NS${capitalize(key)}`, value]),
|
|
1474
|
-
);
|
|
1475
|
-
await Promise.all([
|
|
1476
|
-
this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", updateNs),
|
|
1477
|
-
this.project.ios.updateInfoPlist(this.iosTargetName, "Release", updateNs),
|
|
1478
|
-
]);
|
|
1479
|
-
}
|
|
1480
|
-
async #setUrlSchemesInIos(schemes: string[]) {
|
|
1481
|
-
const urlTypes = schemes.map((scheme) => ({
|
|
1482
|
-
CFBundleURLName: this.target.appId,
|
|
1483
|
-
CFBundleURLSchemes: [scheme],
|
|
1484
|
-
}));
|
|
1485
|
-
await Promise.all([
|
|
1486
|
-
this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", { CFBundleURLTypes: urlTypes }),
|
|
1487
|
-
this.project.ios.updateInfoPlist(this.iosTargetName, "Release", { CFBundleURLTypes: urlTypes }),
|
|
1488
|
-
]);
|
|
1489
|
-
}
|
|
1490
|
-
#serializeIosEntitlements(entitlements: Record<string, string | string[]>) {
|
|
1491
|
-
const lines: string[] = [];
|
|
1492
|
-
for (const [key, value] of Object.entries(entitlements)) {
|
|
1493
|
-
lines.push(` <key>${key}</key>`);
|
|
1494
|
-
if (Array.isArray(value)) {
|
|
1495
|
-
lines.push(" <array>", ...value.map((item) => ` <string>${item}</string>`), " </array>");
|
|
1496
|
-
} else {
|
|
1497
|
-
lines.push(` <string>${value}</string>`);
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
return lines;
|
|
1501
|
-
}
|
|
1502
|
-
//* Write the accumulated iOS entitlements (from deep links + plugins) to App/App.entitlements
|
|
1503
|
-
//* once per prepare. Skipped entirely when nothing contributed entitlements.
|
|
1504
|
-
async #flushIosEntitlements() {
|
|
1505
|
-
if (Object.keys(this.#iosEntitlements).length === 0) return;
|
|
1506
|
-
const entitlementsRelPath = "App/App.entitlements";
|
|
1507
|
-
const entitlementsPath = path.join(this.app.cwdPath, this.iosProjectPath, entitlementsRelPath);
|
|
1508
|
-
const body = [
|
|
1509
|
-
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
1510
|
-
'<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
|
|
1511
|
-
'<plist version="1.0">',
|
|
1512
|
-
"<dict>",
|
|
1513
|
-
...this.#serializeIosEntitlements(this.#iosEntitlements),
|
|
1514
|
-
"</dict>",
|
|
1515
|
-
"</plist>",
|
|
1516
|
-
"",
|
|
1517
|
-
].join("\n");
|
|
1518
|
-
const currentBody = (await Bun.file(entitlementsPath).exists())
|
|
1519
|
-
? await Bun.file(entitlementsPath).text()
|
|
1520
|
-
: undefined;
|
|
1521
|
-
if (currentBody !== body) await writeFile(entitlementsPath, body);
|
|
1522
|
-
await this.#setCodeSignEntitlementsInIos(entitlementsRelPath);
|
|
1523
|
-
}
|
|
1524
|
-
async #setCodeSignEntitlementsInIos(entitlementsRelPath: string) {
|
|
1525
|
-
const pbxprojPath = path.join(this.app.cwdPath, this.iosProjectPath, "App.xcodeproj/project.pbxproj");
|
|
1526
|
-
const lines = (await readFile(pbxprojPath, "utf8")).split("\n");
|
|
1527
|
-
let changed = false;
|
|
1528
|
-
for (let index = 0; index < lines.length; index++) {
|
|
1529
|
-
const line = lines[index] ?? "";
|
|
1530
|
-
if (!line.includes(`PRODUCT_BUNDLE_IDENTIFIER = ${this.target.appId};`)) continue;
|
|
1531
|
-
let start = index;
|
|
1532
|
-
while (start >= 0 && !lines[start]?.includes("buildSettings = {")) start--;
|
|
1533
|
-
let end = index;
|
|
1534
|
-
while (end < lines.length && !/^\s*\};\s*$/.test(lines[end] ?? "")) end++;
|
|
1535
|
-
const settings = lines.slice(start, end + 1);
|
|
1536
|
-
const configName = lines.slice(end + 1, end + 8).find((setting) => setting.includes("name = ")) ?? "";
|
|
1537
|
-
const indent = line.match(/^\s*/)?.[0] ?? "";
|
|
1538
|
-
const insertSettings = [];
|
|
1539
|
-
if (!settings.some((setting) => setting.includes("CODE_SIGN_ENTITLEMENTS"))) {
|
|
1540
|
-
insertSettings.push(`${indent}CODE_SIGN_ENTITLEMENTS = ${entitlementsRelPath};`);
|
|
1541
|
-
}
|
|
1542
|
-
if (
|
|
1543
|
-
configName.includes("name = Debug;") &&
|
|
1544
|
-
!settings.some((setting) => setting.includes("CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION"))
|
|
1545
|
-
) {
|
|
1546
|
-
insertSettings.push(`${indent}CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES;`);
|
|
1547
|
-
}
|
|
1548
|
-
if (insertSettings.length > 0) {
|
|
1549
|
-
lines.splice(index, 0, ...insertSettings);
|
|
1550
|
-
index += insertSettings.length;
|
|
1551
|
-
changed = true;
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
if (changed) await writeFile(pbxprojPath, lines.join("\n"));
|
|
1555
|
-
}
|
|
1556
|
-
async #setCodeSignEntitlementsInIosIfExists(entitlementsRelPath: string) {
|
|
1557
|
-
const entitlementsPath = path.join(this.app.cwdPath, this.iosProjectPath, entitlementsRelPath);
|
|
1558
|
-
if (!(await Bun.file(entitlementsPath).exists())) return;
|
|
1559
|
-
await this.#setCodeSignEntitlementsInIos(entitlementsRelPath);
|
|
1560
|
-
}
|
|
1561
|
-
async #setUrlSchemesInAndroid(schemes: string[]) {
|
|
1562
|
-
const manifestPath = path.join(this.app.cwdPath, this.androidRootPath, "app/src/main/AndroidManifest.xml");
|
|
1563
|
-
let manifest = await readFile(manifestPath, "utf8");
|
|
1564
|
-
let changed = false;
|
|
1565
|
-
for (const scheme of schemes) {
|
|
1566
|
-
if (manifest.includes(`android:scheme="${scheme}"`)) continue;
|
|
1567
|
-
const filter = [
|
|
1568
|
-
" <intent-filter>",
|
|
1569
|
-
' <action android:name="android.intent.action.VIEW" />',
|
|
1570
|
-
' <category android:name="android.intent.category.DEFAULT" />',
|
|
1571
|
-
' <category android:name="android.intent.category.BROWSABLE" />',
|
|
1572
|
-
` <data android:scheme="${scheme}" />`,
|
|
1573
|
-
" </intent-filter>",
|
|
1574
|
-
].join("\n");
|
|
1575
|
-
manifest = manifest.replace(/(\s*<\/activity>)/, `\n${filter}$1`);
|
|
1576
|
-
changed = true;
|
|
1577
|
-
}
|
|
1578
|
-
if (changed) await writeFile(manifestPath, manifest);
|
|
1579
|
-
}
|
|
1580
|
-
async #setDeepLinksInAndroid(schemes: string[], domains: string[]) {
|
|
1581
|
-
await this.#setUrlSchemesInAndroid(schemes);
|
|
1582
|
-
const manifestPath = path.join(this.app.cwdPath, this.androidRootPath, "app/src/main/AndroidManifest.xml");
|
|
1583
|
-
let manifest = await readFile(manifestPath, "utf8");
|
|
1584
|
-
let changed = false;
|
|
1585
|
-
const pathPrefix = resolveMobilePath(this.target, "/");
|
|
1586
|
-
for (const domain of domains) {
|
|
1587
|
-
if (manifest.includes(`android:host="${domain}"`) && manifest.includes('android:scheme="https"')) continue;
|
|
1588
|
-
const filter = [
|
|
1589
|
-
' <intent-filter android:autoVerify="true">',
|
|
1590
|
-
' <action android:name="android.intent.action.VIEW" />',
|
|
1591
|
-
' <category android:name="android.intent.category.DEFAULT" />',
|
|
1592
|
-
' <category android:name="android.intent.category.BROWSABLE" />',
|
|
1593
|
-
` <data android:scheme="https" android:host="${domain}" android:pathPrefix="${pathPrefix}" />`,
|
|
1594
|
-
" </intent-filter>",
|
|
1595
|
-
].join("\n");
|
|
1596
|
-
manifest = manifest.replace(/(\s*<\/activity>)/, `\n${filter}$1`);
|
|
1597
|
-
changed = true;
|
|
1598
|
-
}
|
|
1599
|
-
if (changed) await writeFile(manifestPath, manifest);
|
|
1600
|
-
}
|
|
1601
|
-
#setFeaturesInAndroid(features: string[]) {
|
|
1602
|
-
for (const feature of features) {
|
|
1603
|
-
if (this.#hasFeatureInAndroid(feature)) {
|
|
1604
|
-
this.app.logger.info(`${feature} already exists in android`);
|
|
1605
|
-
return this;
|
|
1606
|
-
}
|
|
1607
|
-
this.app.logger.info(`Adding ${feature} to android`);
|
|
1608
|
-
this.project.android
|
|
1609
|
-
.getAndroidManifest()
|
|
1610
|
-
.injectFragment("manifest", `<uses-feature android:name="${feature}" />`);
|
|
1611
|
-
}
|
|
1612
|
-
return this;
|
|
1613
|
-
}
|
|
1614
|
-
#getFeaturesInAndroid() {
|
|
1615
|
-
const androidManifest = this.project.android.getAndroidManifest();
|
|
1616
|
-
const element = androidManifest.getDocumentElement();
|
|
1617
|
-
if (!element) throw new Error("manifest not found");
|
|
1618
|
-
const usesFeature = element.getElementsByTagName("uses-feature");
|
|
1619
|
-
return Array.from(usesFeature).map((feature) => feature.getAttribute("android:name"));
|
|
1620
|
-
}
|
|
1621
|
-
#hasFeatureInAndroid(feature: string) {
|
|
1622
|
-
return this.#getFeaturesInAndroid().includes(feature);
|
|
1623
|
-
}
|
|
1624
|
-
|
|
1625
|
-
#setPermissionsInAndroid(permissions: string[]) {
|
|
1626
|
-
for (const permission of permissions) {
|
|
1627
|
-
if (this.#hasPermissionInAndroid(permission)) {
|
|
1628
|
-
this.app.logger.info(`${permission} already exists in android`);
|
|
1629
|
-
continue;
|
|
1630
|
-
}
|
|
1631
|
-
this.app.logger.info(`Adding ${permission} to android`);
|
|
1632
|
-
this.project.android
|
|
1633
|
-
.getAndroidManifest()
|
|
1634
|
-
.injectFragment("manifest", `<uses-permission android:name="android.permission.${permission}" />`);
|
|
1635
|
-
}
|
|
1636
|
-
return this;
|
|
1637
|
-
}
|
|
1638
|
-
#getPermissionsInAndroid() {
|
|
1639
|
-
const androidManifest = this.project.android.getAndroidManifest();
|
|
1640
|
-
const element = androidManifest.getDocumentElement();
|
|
1641
|
-
if (!element) throw new Error("manifest not found");
|
|
1642
|
-
const usesPermission = element.getElementsByTagName("uses-permission");
|
|
1643
|
-
return Array.from(usesPermission).map((permission) => permission.getAttribute("android:name"));
|
|
1644
|
-
}
|
|
1645
|
-
#hasPermissionInAndroid(permission: string) {
|
|
1646
|
-
return this.#getPermissionsInAndroid().includes(`android.permission.${permission}`);
|
|
1647
|
-
}
|
|
1648
|
-
}
|