@akanjs/devkit 1.0.20 → 2.1.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +65 -0
- package/README.md +62 -6
- package/aiEditor.ts +304 -0
- package/akanApp/akanApp.host.ts +393 -0
- package/akanApp/index.ts +1 -0
- package/akanConfig/akanConfig.test.ts +236 -0
- package/akanConfig/akanConfig.ts +384 -0
- package/akanConfig/index.ts +2 -0
- package/akanConfig/types.ts +23 -0
- package/applicationBuildReporter.ts +69 -0
- package/applicationBuildRunner.ts +302 -0
- package/applicationReleasePackager.ts +206 -0
- package/artifact/implicitRootLayout.ts +155 -0
- package/artifact/index.ts +1 -0
- package/artifact/routeSeedIndex.test.ts +98 -0
- package/artifact/routeSeedIndex.ts +130 -0
- package/auth.ts +41 -0
- package/builder.ts +164 -0
- package/capacitor.base.config.ts +88 -0
- package/capacitorApp.ts +440 -0
- package/commandDecorators/argMeta.ts +102 -0
- package/commandDecorators/command.ts +351 -0
- package/commandDecorators/commandBuilder.ts +224 -0
- package/commandDecorators/commandDecorators.test.ts +212 -0
- package/commandDecorators/commandMeta.ts +7 -0
- package/commandDecorators/dependencyBuilder.ts +100 -0
- package/{esm/src/commandDecorators/helpFormatter.js → commandDecorators/helpFormatter.ts} +100 -47
- package/{esm/src/commandDecorators/index.js → commandDecorators/index.ts} +4 -2
- package/commandDecorators/targetMeta.ts +31 -0
- package/commandDecorators/types.ts +10 -0
- package/constants.ts +25 -0
- package/createTunnel.ts +36 -0
- package/dependencyScanner.ts +357 -0
- package/devkitUtils.test.ts +259 -0
- package/executors.test.ts +315 -0
- package/executors.ts +1390 -0
- package/{esm/src/extractDeps.js → extractDeps.ts} +26 -20
- package/{esm/src/fileEditor.js → fileEditor.ts} +51 -32
- package/fileSys.ts +39 -0
- package/frontendBuild/allRoutesBuilder.ts +103 -0
- package/frontendBuild/buildRouteClient.test.ts +190 -0
- package/frontendBuild/clientBuildTypes.ts +114 -0
- package/frontendBuild/clientEntriesBundler.ts +303 -0
- package/frontendBuild/clientEntryDiscovery.ts +199 -0
- package/frontendBuild/csrArtifactBuilder.ts +237 -0
- package/frontendBuild/cssCompiler.ts +286 -0
- package/frontendBuild/cssImportResolver.ts +116 -0
- package/frontendBuild/fontOptimizer.ts +427 -0
- package/frontendBuild/frontendBuild.test.ts +204 -0
- package/frontendBuild/hmrChangeClassifier.ts +28 -0
- package/frontendBuild/hmrWatcher.ts +102 -0
- package/frontendBuild/index.ts +18 -0
- package/frontendBuild/pagesBundleBuilder.ts +137 -0
- package/frontendBuild/pagesEntrySourceGenerator.ts +37 -0
- package/frontendBuild/precompressArtifacts.ts +59 -0
- package/frontendBuild/routeClientBuilder.ts +290 -0
- package/frontendBuild/routesManifestArtifactSerializer.ts +62 -0
- package/frontendBuild/ssrBaseArtifactBuilder.ts +139 -0
- package/frontendBuild/vendorSpecifiers.ts +16 -0
- package/frontendBuild/watchRootResolver.ts +28 -0
- package/getCredentials.ts +19 -0
- package/getDirname.ts +3 -0
- package/getModelFileData.ts +59 -0
- package/getRelatedCnsts.ts +313 -0
- package/guideline.ts +19 -0
- package/incrementalBuilder/incrementalBuilder.host.test.ts +51 -0
- package/incrementalBuilder/incrementalBuilder.host.ts +152 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +331 -0
- package/incrementalBuilder/index.ts +1 -0
- package/{esm/src/index.js → index.ts} +28 -15
- package/lint/no-deep-internal-import.grit +25 -0
- package/lint/no-import-client-functions.grit +32 -0
- package/lint/no-import-external-library.grit +21 -0
- package/lint/no-js-private-class-method.grit +42 -0
- package/lint/no-use-client-in-server.grit +7 -0
- package/lint/non-scalar-props-restricted.grit +13 -0
- package/linter.ts +271 -0
- package/mobile/index.ts +1 -0
- package/mobile/mobileTarget.test.ts +53 -0
- package/mobile/mobileTarget.ts +88 -0
- package/package.json +48 -31
- package/prompter.ts +72 -0
- package/scanInfo.ts +606 -0
- package/selectModel.ts +11 -0
- package/{esm/src/spinner.js → spinner.ts} +22 -28
- package/{esm/src/capacitorApp.js → src/capacitorApp.ts} +82 -81
- package/sshTunnel.ts +152 -0
- package/{esm/src/streamAi.js → streamAi.ts} +18 -12
- package/transforms/barrelAnalyzer.ts +278 -0
- package/transforms/barrelImportsPlugin.ts +504 -0
- package/transforms/externalizeFrameworkPlugin.ts +185 -0
- package/transforms/index.ts +5 -0
- package/transforms/rscUseClientTransform.ts +59 -0
- package/transforms/transforms.test.ts +208 -0
- package/transforms/useClientBundlePlugin.ts +47 -0
- package/tsconfig.json +37 -0
- package/typeChecker.ts +264 -0
- package/types.ts +44 -0
- package/ui/MultiScrollList.tsx +242 -0
- package/ui/ScrollList.tsx +107 -0
- package/ui/index.ts +2 -0
- package/{esm/src/uploadRelease.js → uploadRelease.ts} +50 -34
- package/{esm/src/useStdoutDimensions.js → useStdoutDimensions.ts} +5 -5
- package/cjs/index.js +0 -21
- package/cjs/src/aiEditor.js +0 -311
- package/cjs/src/auth.js +0 -72
- package/cjs/src/builder.js +0 -114
- package/cjs/src/capacitorApp.js +0 -313
- package/cjs/src/commandDecorators/argMeta.js +0 -88
- package/cjs/src/commandDecorators/command.js +0 -324
- package/cjs/src/commandDecorators/commandMeta.js +0 -30
- package/cjs/src/commandDecorators/helpFormatter.js +0 -211
- package/cjs/src/commandDecorators/index.js +0 -31
- package/cjs/src/commandDecorators/targetMeta.js +0 -57
- package/cjs/src/commandDecorators/types.js +0 -15
- package/cjs/src/constants.js +0 -46
- package/cjs/src/createTunnel.js +0 -49
- package/cjs/src/dependencyScanner.js +0 -220
- package/cjs/src/executors.js +0 -964
- package/cjs/src/extractDeps.js +0 -103
- package/cjs/src/fileEditor.js +0 -120
- package/cjs/src/getCredentials.js +0 -44
- package/cjs/src/getDirname.js +0 -38
- package/cjs/src/getModelFileData.js +0 -66
- package/cjs/src/getRelatedCnsts.js +0 -260
- package/cjs/src/guideline.js +0 -15
- package/cjs/src/index.js +0 -65
- package/cjs/src/linter.js +0 -238
- package/cjs/src/prompter.js +0 -85
- package/cjs/src/scanInfo.js +0 -491
- package/cjs/src/selectModel.js +0 -46
- package/cjs/src/spinner.js +0 -93
- package/cjs/src/streamAi.js +0 -62
- package/cjs/src/typeChecker.js +0 -207
- package/cjs/src/types.js +0 -15
- package/cjs/src/uploadRelease.js +0 -112
- package/cjs/src/useStdoutDimensions.js +0 -43
- package/esm/index.js +0 -1
- package/esm/src/aiEditor.js +0 -282
- package/esm/src/auth.js +0 -42
- package/esm/src/builder.js +0 -81
- package/esm/src/commandDecorators/argMeta.js +0 -54
- package/esm/src/commandDecorators/command.js +0 -290
- package/esm/src/commandDecorators/commandMeta.js +0 -7
- package/esm/src/commandDecorators/targetMeta.js +0 -33
- package/esm/src/commandDecorators/types.js +0 -0
- package/esm/src/constants.js +0 -17
- package/esm/src/createTunnel.js +0 -26
- package/esm/src/dependencyScanner.js +0 -187
- package/esm/src/executors.js +0 -928
- package/esm/src/getCredentials.js +0 -11
- package/esm/src/getDirname.js +0 -5
- package/esm/src/getModelFileData.js +0 -33
- package/esm/src/getRelatedCnsts.js +0 -221
- package/esm/src/guideline.js +0 -0
- package/esm/src/linter.js +0 -205
- package/esm/src/prompter.js +0 -51
- package/esm/src/scanInfo.js +0 -455
- package/esm/src/selectModel.js +0 -13
- package/esm/src/typeChecker.js +0 -174
- package/esm/src/types.js +0 -0
- package/index.d.ts +0 -1
- package/src/aiEditor.d.ts +0 -50
- package/src/auth.d.ts +0 -9
- package/src/builder.d.ts +0 -18
- package/src/capacitorApp.d.ts +0 -39
- package/src/commandDecorators/argMeta.d.ts +0 -67
- package/src/commandDecorators/command.d.ts +0 -2
- package/src/commandDecorators/commandMeta.d.ts +0 -2
- package/src/commandDecorators/helpFormatter.d.ts +0 -3
- package/src/commandDecorators/index.d.ts +0 -6
- package/src/commandDecorators/targetMeta.d.ts +0 -19
- package/src/commandDecorators/types.d.ts +0 -1
- package/src/constants.d.ts +0 -26
- package/src/createTunnel.d.ts +0 -8
- package/src/dependencyScanner.d.ts +0 -23
- package/src/executors.d.ts +0 -296
- package/src/extractDeps.d.ts +0 -7
- package/src/fileEditor.d.ts +0 -16
- package/src/getCredentials.d.ts +0 -12
- package/src/getDirname.d.ts +0 -1
- package/src/getModelFileData.d.ts +0 -16
- package/src/getRelatedCnsts.d.ts +0 -53
- package/src/guideline.d.ts +0 -19
- package/src/index.d.ts +0 -23
- package/src/linter.d.ts +0 -109
- package/src/prompter.d.ts +0 -14
- package/src/scanInfo.d.ts +0 -82
- package/src/selectModel.d.ts +0 -1
- package/src/spinner.d.ts +0 -20
- package/src/streamAi.d.ts +0 -6
- package/src/typeChecker.d.ts +0 -52
- package/src/types.d.ts +0 -31
- package/src/uploadRelease.d.ts +0 -10
- package/src/useStdoutDimensions.d.ts +0 -1
package/capacitorApp.ts
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
import { cp, mkdir, rm } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { MobileProject } from "@trapezedev/project";
|
|
4
|
+
import type { AndroidProject } from "@trapezedev/project/dist/android/project";
|
|
5
|
+
import type { IosProject } from "@trapezedev/project/dist/ios/project";
|
|
6
|
+
import { capitalize } from "akanjs/common";
|
|
7
|
+
import type { AkanMobileTargetConfig } from "./akanConfig";
|
|
8
|
+
import type { AppExecutor } from "./executors";
|
|
9
|
+
import { FileEditor } from "./fileEditor";
|
|
10
|
+
import { resolveMobilePath, targetHtmlFilename } from "./mobile";
|
|
11
|
+
|
|
12
|
+
interface RunConfig {
|
|
13
|
+
operation: "local" | "release";
|
|
14
|
+
env: "local" | "debug" | "develop" | "main";
|
|
15
|
+
regenerate?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface PrepareConfig extends RunConfig {}
|
|
19
|
+
|
|
20
|
+
export class CapacitorApp {
|
|
21
|
+
project: MobileProject & { ios: IosProject; android: AndroidProject };
|
|
22
|
+
iosTargetName = "App";
|
|
23
|
+
readonly targetRoot: string;
|
|
24
|
+
readonly targetRootPath: string;
|
|
25
|
+
readonly targetWebRoot: string;
|
|
26
|
+
readonly targetAssetRoot: string;
|
|
27
|
+
readonly iosRootPath = "ios";
|
|
28
|
+
readonly iosProjectPath = "ios/App";
|
|
29
|
+
readonly androidRootPath = "android";
|
|
30
|
+
constructor(
|
|
31
|
+
private readonly app: AppExecutor,
|
|
32
|
+
readonly target: AkanMobileTargetConfig,
|
|
33
|
+
) {
|
|
34
|
+
this.targetRootPath = path.posix.join("mobile", this.target.name);
|
|
35
|
+
this.targetRoot = path.join(this.app.cwdPath, this.targetRootPath);
|
|
36
|
+
this.targetWebRoot = path.join(this.targetRoot, "www");
|
|
37
|
+
this.targetAssetRoot = path.join(this.targetRoot, "assets");
|
|
38
|
+
this.project = new MobileProject(this.app.cwdPath, {
|
|
39
|
+
android: { path: this.androidRootPath },
|
|
40
|
+
ios: { path: this.iosProjectPath },
|
|
41
|
+
}) as MobileProject & { ios: IosProject; android: AndroidProject };
|
|
42
|
+
}
|
|
43
|
+
async init({
|
|
44
|
+
platform,
|
|
45
|
+
operation = "release",
|
|
46
|
+
env = "debug",
|
|
47
|
+
regenerate = false,
|
|
48
|
+
}: { platform?: "ios" | "android" } & Partial<PrepareConfig> = {}) {
|
|
49
|
+
await mkdir(this.targetRoot, { recursive: true });
|
|
50
|
+
await this.#writeCapacitorConfig();
|
|
51
|
+
if (regenerate) {
|
|
52
|
+
if (!platform || platform === "ios")
|
|
53
|
+
await rm(path.join(this.app.cwdPath, this.iosRootPath), { recursive: true, force: true });
|
|
54
|
+
if (!platform || platform === "android")
|
|
55
|
+
await rm(path.join(this.app.cwdPath, this.androidRootPath), { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
const project = this.project as MobileProject;
|
|
58
|
+
await this.project.load();
|
|
59
|
+
if ((!platform || platform === "android") && !project.android) {
|
|
60
|
+
await this.#spawnMobile("npx", ["cap", "add", "android"], { operation, env });
|
|
61
|
+
await this.project.load();
|
|
62
|
+
}
|
|
63
|
+
if ((!platform || platform === "ios") && !project.ios) {
|
|
64
|
+
await this.#spawnMobile("npx", ["cap", "add", "ios"], { operation, env });
|
|
65
|
+
await this.project.load();
|
|
66
|
+
}
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
async save() {
|
|
70
|
+
await this.project.commit();
|
|
71
|
+
}
|
|
72
|
+
async #prepareIos({ operation, env, regenerate = false }: PrepareConfig) {
|
|
73
|
+
await this.init({ platform: "ios", operation, env, regenerate });
|
|
74
|
+
await this.#prepareTargetAssets();
|
|
75
|
+
await this.#prepareExternalFiles("ios");
|
|
76
|
+
await this.#applyIosMetadata();
|
|
77
|
+
await this.#applyPermissions();
|
|
78
|
+
await this.#applyLinks();
|
|
79
|
+
await this.project.commit();
|
|
80
|
+
await this.#generateAssets({ operation, env });
|
|
81
|
+
this.app.verbose(`syncing iOS`);
|
|
82
|
+
await this.#spawnMobile("npx", ["cap", "sync", "ios"], { operation, env });
|
|
83
|
+
this.app.verbose(`sync completed.`);
|
|
84
|
+
}
|
|
85
|
+
async buildIos({ env = "debug", regenerate = false }: { env?: RunConfig["env"]; regenerate?: boolean } = {}) {
|
|
86
|
+
await this.prepareWww();
|
|
87
|
+
await this.#prepareIos({ operation: "release", env, regenerate });
|
|
88
|
+
await this.#spawnMobile("npx", ["cap", "build", "ios"], { operation: "release", env }, { stdio: "inherit" });
|
|
89
|
+
this.app.verbose(`build completed iOS.`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
async syncIos() {
|
|
93
|
+
await this.#spawnMobile("npx", ["cap", "sync", "ios"], { operation: "local", env: "local" });
|
|
94
|
+
}
|
|
95
|
+
async openIos() {
|
|
96
|
+
await this.#spawnMobile("npx", ["cap", "open", "ios"], { operation: "local", env: "local" });
|
|
97
|
+
}
|
|
98
|
+
async runIos({ operation, env, regenerate = false }: RunConfig) {
|
|
99
|
+
if (operation === "release") await this.prepareWww();
|
|
100
|
+
await this.#prepareIos({ operation, env, regenerate });
|
|
101
|
+
const args = ["cap", "run", "ios"];
|
|
102
|
+
await this.#spawnMobile("npx", args, { operation, env }, { stdio: "inherit" });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async #prepareAndroid({ operation, env, regenerate = false }: PrepareConfig) {
|
|
106
|
+
await this.init({ platform: "android", operation, env, regenerate });
|
|
107
|
+
await this.#prepareTargetAssets();
|
|
108
|
+
await this.#prepareExternalFiles("android");
|
|
109
|
+
await this.#applyAndroidMetadata();
|
|
110
|
+
await this.#applyPermissions();
|
|
111
|
+
await this.#applyLinks();
|
|
112
|
+
await this.project.commit();
|
|
113
|
+
await this.#generateAssets({ operation, env });
|
|
114
|
+
await this.#spawnMobile("npx", ["cap", "sync", "android"], { operation, env });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async #updateAndroidBuildTypes() {
|
|
118
|
+
//keystore 기본 설정 및 debug, release 설정
|
|
119
|
+
|
|
120
|
+
const appGradle = await FileEditor.create(path.join(this.app.cwdPath, this.androidRootPath, "app/build.gradle"));
|
|
121
|
+
const buildTypesBlock = `
|
|
122
|
+
debug {
|
|
123
|
+
applicationIdSuffix ".debug"
|
|
124
|
+
versionNameSuffix "-DEBUG"
|
|
125
|
+
debuggable true
|
|
126
|
+
minifyEnabled false
|
|
127
|
+
}
|
|
128
|
+
`;
|
|
129
|
+
const singinConfigBlock = `
|
|
130
|
+
signingConfigs {
|
|
131
|
+
debug {
|
|
132
|
+
storeFile file('debug.keystore')
|
|
133
|
+
storePassword 'android'
|
|
134
|
+
keyAlias 'androiddebugkey'
|
|
135
|
+
keyPassword 'android'
|
|
136
|
+
}
|
|
137
|
+
release {
|
|
138
|
+
if (project.hasProperty('MYAPP_RELEASE_STORE_FILE')) {
|
|
139
|
+
storeFile file(MYAPP_RELEASE_STORE_FILE)
|
|
140
|
+
storePassword MYAPP_RELEASE_STORE_PASSWORD
|
|
141
|
+
keyAlias MYAPP_RELEASE_KEY_ALIAS
|
|
142
|
+
keyPassword MYAPP_RELEASE_KEY_PASSWORD
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
`;
|
|
147
|
+
if (appGradle.find("signingConfigs {") === -1) {
|
|
148
|
+
appGradle.insertBefore("buildTypes {", singinConfigBlock);
|
|
149
|
+
}
|
|
150
|
+
if (appGradle.find(`applicationIdSuffix ".debug"`) === -1) {
|
|
151
|
+
appGradle.insertAfter("buildTypes {", buildTypesBlock);
|
|
152
|
+
}
|
|
153
|
+
await appGradle.save();
|
|
154
|
+
}
|
|
155
|
+
async buildAndroid(
|
|
156
|
+
assembleType: "apk" | "aab",
|
|
157
|
+
{ env = "debug", regenerate = false }: { env?: RunConfig["env"]; regenerate?: boolean } = {},
|
|
158
|
+
) {
|
|
159
|
+
await this.prepareWww();
|
|
160
|
+
await this.#prepareAndroid({ operation: "release", env, regenerate });
|
|
161
|
+
await this.#updateAndroidBuildTypes();
|
|
162
|
+
//윈도우는 gradlew.bat 사용
|
|
163
|
+
const isWindows = process.platform === "win32";
|
|
164
|
+
const gradleCommand = isWindows ? "gradlew.bat" : "./gradlew";
|
|
165
|
+
|
|
166
|
+
await this.app.spawn(gradleCommand, [assembleType === "apk" ? "assembleRelease" : "bundleRelease"], {
|
|
167
|
+
stdio: "inherit",
|
|
168
|
+
cwd: path.join(this.app.cwdPath, this.androidRootPath),
|
|
169
|
+
env: this.#commandEnv("release", env),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
async openAndroid() {
|
|
173
|
+
await this.#spawnMobile("npx", ["cap", "open", "android"], { operation: "local", env: "local" });
|
|
174
|
+
}
|
|
175
|
+
async syncAndroid(options: { regenerate?: boolean } = {}) {
|
|
176
|
+
await this.prepareWww();
|
|
177
|
+
await this.#prepareAndroid({ operation: "release", env: "debug", ...options });
|
|
178
|
+
this.app.log(`Sync Android Completed.`);
|
|
179
|
+
}
|
|
180
|
+
async runAndroid({ operation, env, regenerate = false }: RunConfig) {
|
|
181
|
+
if (operation === "release") await this.prepareWww();
|
|
182
|
+
await this.#prepareAndroid({ operation, env, regenerate });
|
|
183
|
+
this.app.logger.info(`Running Android in ${operation} mode on ${env} env`);
|
|
184
|
+
const args = ["cap", "run", "android"];
|
|
185
|
+
await this.#spawnMobile("npx", args, { operation, env }, { stdio: "inherit" });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async releaseIos() {
|
|
189
|
+
await this.prepareWww();
|
|
190
|
+
await this.#prepareIos({ operation: "release", env: "main" });
|
|
191
|
+
}
|
|
192
|
+
async releaseAndroid() {
|
|
193
|
+
await this.prepareWww();
|
|
194
|
+
await this.#prepareAndroid({ operation: "release", env: "main" });
|
|
195
|
+
}
|
|
196
|
+
async prepareWww() {
|
|
197
|
+
const htmlSource = path.join(this.app.dist.cwdPath, "csr", targetHtmlFilename(this.target));
|
|
198
|
+
if (!(await Bun.file(htmlSource).exists()))
|
|
199
|
+
throw new Error(`CSR html for mobile target '${this.target.name}' not found: ${htmlSource}`);
|
|
200
|
+
await rm(this.targetWebRoot, { recursive: true, force: true });
|
|
201
|
+
await mkdir(this.targetWebRoot, { recursive: true });
|
|
202
|
+
await Bun.write(
|
|
203
|
+
path.join(this.targetWebRoot, "index.html"),
|
|
204
|
+
this.#injectMobileTargetMeta(await Bun.file(htmlSource).text()),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
#injectMobileTargetMeta(html: string) {
|
|
208
|
+
const basePath = this.target.basePath?.replace(/^\/+|\/+$/g, "") ?? "";
|
|
209
|
+
const script = `<script>window.__AKAN_MOBILE_TARGET__=${JSON.stringify({ name: this.target.name, basePath })};</script>`;
|
|
210
|
+
if (html.includes("window.__AKAN_MOBILE_TARGET__")) return html;
|
|
211
|
+
return html.replace(/<\/head\s*>/i, `${script}\n</head>`);
|
|
212
|
+
}
|
|
213
|
+
async #writeCapacitorConfig() {
|
|
214
|
+
await mkdir(this.targetRoot, { recursive: true });
|
|
215
|
+
const appInfoPath = path
|
|
216
|
+
.relative(this.app.cwdPath, path.join(this.app.cwdPath, "akan.app.json"))
|
|
217
|
+
.split(path.sep)
|
|
218
|
+
.join("/");
|
|
219
|
+
const content = `import type { AppScanResult } from "akanjs";
|
|
220
|
+
import { withBase } from "@akanjs/devkit/capacitor.base.config";
|
|
221
|
+
import appInfo from "${appInfoPath.startsWith(".") ? appInfoPath : `./${appInfoPath}`}";
|
|
222
|
+
|
|
223
|
+
export default withBase(
|
|
224
|
+
(config, target) => ({
|
|
225
|
+
...config,
|
|
226
|
+
webDir: \`mobile/\${target.name}/www\`,
|
|
227
|
+
android: {
|
|
228
|
+
...config.android,
|
|
229
|
+
path: "android",
|
|
230
|
+
},
|
|
231
|
+
ios: {
|
|
232
|
+
...config.ios,
|
|
233
|
+
path: "ios",
|
|
234
|
+
},
|
|
235
|
+
}),
|
|
236
|
+
appInfo as AppScanResult,
|
|
237
|
+
);
|
|
238
|
+
`;
|
|
239
|
+
await Bun.write(path.join(this.app.cwdPath, "capacitor.config.ts"), content);
|
|
240
|
+
}
|
|
241
|
+
async #prepareTargetAssets() {
|
|
242
|
+
if (!this.target.assets) return;
|
|
243
|
+
await mkdir(this.targetAssetRoot, { recursive: true });
|
|
244
|
+
if (this.target.assets.icon)
|
|
245
|
+
await cp(path.join(this.app.cwdPath, this.target.assets.icon), path.join(this.targetAssetRoot, "icon.png"), {
|
|
246
|
+
force: true,
|
|
247
|
+
});
|
|
248
|
+
if (this.target.assets.splash)
|
|
249
|
+
await cp(path.join(this.app.cwdPath, this.target.assets.splash), path.join(this.targetAssetRoot, "splash.png"), {
|
|
250
|
+
force: true,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
async #prepareExternalFiles(platform: "ios" | "android") {
|
|
254
|
+
const files = this.target.files?.[platform];
|
|
255
|
+
if (!files) return;
|
|
256
|
+
const platformRoot = path.join(this.app.cwdPath, platform === "ios" ? this.iosRootPath : this.androidRootPath);
|
|
257
|
+
await Promise.all(
|
|
258
|
+
Object.entries(files).map(async ([to, from]) => {
|
|
259
|
+
const targetPath = path.join(platformRoot, to);
|
|
260
|
+
await mkdir(path.dirname(targetPath), { recursive: true });
|
|
261
|
+
await cp(path.join(this.app.cwdPath, from), targetPath, { force: true });
|
|
262
|
+
}),
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
async #generateAssets({ operation, env }: Pick<RunConfig, "operation" | "env">) {
|
|
266
|
+
if (!this.target.assets) return;
|
|
267
|
+
await this.#spawnMobile(
|
|
268
|
+
"npx",
|
|
269
|
+
[
|
|
270
|
+
"@capacitor/assets",
|
|
271
|
+
"generate",
|
|
272
|
+
"--assetPath",
|
|
273
|
+
path.posix.join(this.targetRootPath, "assets"),
|
|
274
|
+
"--iosProject",
|
|
275
|
+
this.iosProjectPath,
|
|
276
|
+
"--androidProject",
|
|
277
|
+
this.androidRootPath,
|
|
278
|
+
],
|
|
279
|
+
{ operation, env },
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
async #applyIosMetadata() {
|
|
283
|
+
this.project.ios.setBundleId("App", "Debug", this.target.appId);
|
|
284
|
+
this.project.ios.setBundleId("App", "Release", this.target.appId);
|
|
285
|
+
await this.project.ios.setVersion("App", "Debug", this.target.version);
|
|
286
|
+
await this.project.ios.setVersion("App", "Release", this.target.version);
|
|
287
|
+
await this.project.ios.setBuild("App", "Debug", this.target.buildNum);
|
|
288
|
+
await this.project.ios.setBuild("App", "Release", this.target.buildNum);
|
|
289
|
+
}
|
|
290
|
+
async #applyAndroidMetadata() {
|
|
291
|
+
await this.project.android.setVersionName(this.target.version);
|
|
292
|
+
await this.project.android.setPackageName(this.target.appId);
|
|
293
|
+
await this.project.android.setVersionCode(this.target.buildNum);
|
|
294
|
+
await this.project.android.setAppName(this.target.appName);
|
|
295
|
+
}
|
|
296
|
+
async #applyPermissions() {
|
|
297
|
+
for (const permission of this.target.permissions ?? []) {
|
|
298
|
+
if (permission === "camera") await this.addCamera();
|
|
299
|
+
else if (permission === "contacts") await this.addContact();
|
|
300
|
+
else if (permission === "location") await this.addLocation();
|
|
301
|
+
else if (permission === "push") await this.addPush();
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async #applyLinks() {
|
|
305
|
+
const links = this.target.links;
|
|
306
|
+
if (!links) return;
|
|
307
|
+
const schemes = links.schemes ?? [];
|
|
308
|
+
if (schemes.length > 0) {
|
|
309
|
+
await this.#setPermissionInIos({
|
|
310
|
+
appTransportSecurity: "",
|
|
311
|
+
});
|
|
312
|
+
for (const scheme of schemes) {
|
|
313
|
+
this.project.android
|
|
314
|
+
.getAndroidManifest()
|
|
315
|
+
.injectFragment(
|
|
316
|
+
"activity",
|
|
317
|
+
`<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>`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
for (const domain of links.associatedDomains ?? []) {
|
|
322
|
+
this.app.logger.info(`Configure iOS associated domain manually if needed: ${domain}`);
|
|
323
|
+
}
|
|
324
|
+
for (const host of links.androidHosts ?? []) {
|
|
325
|
+
const pathPrefix = resolveMobilePath(this.target, "/");
|
|
326
|
+
this.project.android
|
|
327
|
+
.getAndroidManifest()
|
|
328
|
+
.injectFragment(
|
|
329
|
+
"activity",
|
|
330
|
+
`<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="${host}" android:pathPrefix="${pathPrefix}" /></intent-filter>`,
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
#commandEnv(operation: "local" | "release", env: "local" | "debug" | "develop" | "main") {
|
|
335
|
+
return this.app.getCommandEnv({
|
|
336
|
+
APP_OPERATION_MODE: operation,
|
|
337
|
+
AKAN_PUBLIC_OPERATION_MODE: env === "local" ? "local" : "cloud",
|
|
338
|
+
AKAN_PUBLIC_ENV: env,
|
|
339
|
+
AKAN_MOBILE_TARGET: this.target.name,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
async #spawn(command: string, args: string[] = [], options: Parameters<AppExecutor["spawn"]>[2] = {}) {
|
|
343
|
+
return await this.app.spawn(command, args, { cwd: this.app.cwdPath, ...options });
|
|
344
|
+
}
|
|
345
|
+
async #spawnMobile(
|
|
346
|
+
command: string,
|
|
347
|
+
args: string[] = [],
|
|
348
|
+
{ operation, env }: Pick<RunConfig, "operation" | "env">,
|
|
349
|
+
options: Parameters<AppExecutor["spawn"]>[2] = {},
|
|
350
|
+
) {
|
|
351
|
+
return await this.#spawn(command, args, {
|
|
352
|
+
...options,
|
|
353
|
+
env: { ...this.#commandEnv(operation, env), ...options.env },
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
async addCamera() {
|
|
357
|
+
await this.#setPermissionInIos({
|
|
358
|
+
cameraUsageDescription: "$(PRODUCT_NAME) requires access to the camera to take photos.",
|
|
359
|
+
photoAddUsageDescription: "$(PRODUCT_NAME) requires access to the photo library to take photos.",
|
|
360
|
+
photoUsageDescription: "$(PRODUCT_NAME) requires access to the photo library to take photos.",
|
|
361
|
+
});
|
|
362
|
+
this.#setPermissionsInAndroid(["READ_MEDIA_IMAGES", "READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE"]);
|
|
363
|
+
}
|
|
364
|
+
async addContact() {
|
|
365
|
+
await this.#setPermissionInIos({
|
|
366
|
+
contactsUsageDescription: "$(PRODUCT_NAME) requires access to the contacts to add new contacts.",
|
|
367
|
+
});
|
|
368
|
+
this.#setPermissionsInAndroid(["READ_CONTACTS", "WRITE_CONTACTS"]);
|
|
369
|
+
}
|
|
370
|
+
async addLocation() {
|
|
371
|
+
await this.#setPermissionInIos({
|
|
372
|
+
locationAlwaysUsageDescription: "$(PRODUCT_NAME) requires access to the location to get the user's location.",
|
|
373
|
+
locationWhenInUseUsageDescription: "$(PRODUCT_NAME) requires access to the location to get the user's location.",
|
|
374
|
+
});
|
|
375
|
+
this.#setPermissionsInAndroid(["ACCESS_COARSE_LOCATION", "ACCESS_FINE_LOCATION"]);
|
|
376
|
+
this.#setFeaturesInAndroid(["android.hardware.location.gps"]);
|
|
377
|
+
}
|
|
378
|
+
async addPush() {
|
|
379
|
+
await this.#setPermissionInIos({
|
|
380
|
+
userNotificationsUsageDescription: "$(PRODUCT_NAME) uses notifications to keep you updated.",
|
|
381
|
+
});
|
|
382
|
+
this.#setPermissionsInAndroid(["POST_NOTIFICATIONS"]);
|
|
383
|
+
}
|
|
384
|
+
async #setPermissionInIos(permissions: { [key: string]: string }) {
|
|
385
|
+
const updateNs = Object.fromEntries(
|
|
386
|
+
Object.entries(permissions).map(([key, value]) => [`NS${capitalize(key)}`, value]),
|
|
387
|
+
);
|
|
388
|
+
await Promise.all([
|
|
389
|
+
this.project.ios.updateInfoPlist(this.iosTargetName, "Debug", updateNs),
|
|
390
|
+
this.project.ios.updateInfoPlist(this.iosTargetName, "Release", updateNs),
|
|
391
|
+
]);
|
|
392
|
+
}
|
|
393
|
+
#setFeaturesInAndroid(features: string[]) {
|
|
394
|
+
for (const feature of features) {
|
|
395
|
+
if (this.#hasFeatureInAndroid(feature)) {
|
|
396
|
+
this.app.logger.info(`${feature} already exists in android`);
|
|
397
|
+
return this;
|
|
398
|
+
}
|
|
399
|
+
this.app.logger.info(`Adding ${feature} to android`);
|
|
400
|
+
this.project.android
|
|
401
|
+
.getAndroidManifest()
|
|
402
|
+
.injectFragment("manifest", `<uses-feature android:name="${feature}" />`);
|
|
403
|
+
}
|
|
404
|
+
return this;
|
|
405
|
+
}
|
|
406
|
+
#getFeaturesInAndroid() {
|
|
407
|
+
const androidManifest = this.project.android.getAndroidManifest();
|
|
408
|
+
const element = androidManifest.getDocumentElement();
|
|
409
|
+
if (!element) throw new Error("manifest not found");
|
|
410
|
+
const usesFeature = element.getElementsByTagName("uses-feature");
|
|
411
|
+
return Array.from(usesFeature).map((feature) => feature.getAttribute("android:name"));
|
|
412
|
+
}
|
|
413
|
+
#hasFeatureInAndroid(feature: string) {
|
|
414
|
+
return this.#getFeaturesInAndroid().includes(feature);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
#setPermissionsInAndroid(permissions: string[]) {
|
|
418
|
+
for (const permission of permissions) {
|
|
419
|
+
if (this.#hasPermissionInAndroid(permission)) {
|
|
420
|
+
this.app.logger.info(`${permission} already exists in android`);
|
|
421
|
+
return this;
|
|
422
|
+
}
|
|
423
|
+
this.app.logger.info(`Adding ${permission} to android`);
|
|
424
|
+
this.project.android
|
|
425
|
+
.getAndroidManifest()
|
|
426
|
+
.injectFragment("manifest", `<uses-permission android:name="android.permission.${permission}" />`);
|
|
427
|
+
}
|
|
428
|
+
return this;
|
|
429
|
+
}
|
|
430
|
+
#getPermissionsInAndroid() {
|
|
431
|
+
const androidManifest = this.project.android.getAndroidManifest();
|
|
432
|
+
const element = androidManifest.getDocumentElement();
|
|
433
|
+
if (!element) throw new Error("manifest not found");
|
|
434
|
+
const usesPermission = element.getElementsByTagName("uses-permission");
|
|
435
|
+
return Array.from(usesPermission).map((permission) => permission.getAttribute("android:name"));
|
|
436
|
+
}
|
|
437
|
+
#hasPermissionInAndroid(permission: string) {
|
|
438
|
+
return this.#getPermissionsInAndroid().includes(permission);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AppExecutor,
|
|
3
|
+
Executor,
|
|
4
|
+
LibExecutor,
|
|
5
|
+
ModuleExecutor,
|
|
6
|
+
PkgExecutor,
|
|
7
|
+
SysExecutor,
|
|
8
|
+
WorkspaceExecutor,
|
|
9
|
+
} from "../executors";
|
|
10
|
+
import { COMMAND_META, type CommandCls } from "./targetMeta";
|
|
11
|
+
|
|
12
|
+
export const argTypes = ["Argument", "Option"] as const;
|
|
13
|
+
export type ArgType = (typeof argTypes)[number];
|
|
14
|
+
|
|
15
|
+
export const internalArgTypes = ["Workspace", "App", "Lib", "Sys", "Pkg", "Module", "Exec"] as const;
|
|
16
|
+
export type InternalArgType = (typeof internalArgTypes)[number];
|
|
17
|
+
|
|
18
|
+
export type PrimitiveArgType = StringConstructor | NumberConstructor | BooleanConstructor;
|
|
19
|
+
export type NormalizedPrimitiveArgType = "string" | "number" | "boolean";
|
|
20
|
+
|
|
21
|
+
export type CommandContext = {
|
|
22
|
+
values: Record<string, unknown>;
|
|
23
|
+
app?: AppExecutor;
|
|
24
|
+
lib?: LibExecutor;
|
|
25
|
+
sys?: SysExecutor;
|
|
26
|
+
pkg?: PkgExecutor;
|
|
27
|
+
module?: ModuleExecutor;
|
|
28
|
+
exec?: Executor;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type EnumChoice = string | number | { label: string; value: string | number | boolean };
|
|
32
|
+
export type EnumChoices = readonly EnumChoice[];
|
|
33
|
+
export type DynamicEnum<Context> = (context: Context) => EnumChoices | Promise<EnumChoices>;
|
|
34
|
+
|
|
35
|
+
export interface ArgsOption<Context = CommandContext> {
|
|
36
|
+
type?: "string" | "number" | "boolean";
|
|
37
|
+
flag?: string;
|
|
38
|
+
desc?: string;
|
|
39
|
+
default?: string | number | boolean;
|
|
40
|
+
nullable?: boolean;
|
|
41
|
+
example?: string | number | boolean;
|
|
42
|
+
enum?: EnumChoices | DynamicEnum<Context>;
|
|
43
|
+
ask?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface ArgMeta<Context = CommandContext> {
|
|
46
|
+
name: string;
|
|
47
|
+
argsOption: ArgsOption<Context>;
|
|
48
|
+
key: string;
|
|
49
|
+
idx: number;
|
|
50
|
+
type: ArgType;
|
|
51
|
+
}
|
|
52
|
+
export interface InternalArgMeta {
|
|
53
|
+
key: string;
|
|
54
|
+
idx: number;
|
|
55
|
+
type: InternalArgType;
|
|
56
|
+
option?: { nullable?: boolean };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const getArgMetas = (
|
|
60
|
+
command: CommandCls,
|
|
61
|
+
key: string,
|
|
62
|
+
): [(ArgMeta | InternalArgMeta)[], ArgMeta[], (ArgMeta | InternalArgMeta)[]] => {
|
|
63
|
+
const allArgMetas = [...(command[COMMAND_META]?.get(key)?.args ?? [])];
|
|
64
|
+
const argMetas = allArgMetas.filter((argMeta): argMeta is ArgMeta => argMeta.type === "Option");
|
|
65
|
+
const internalArgMetas = allArgMetas.filter((argMeta) => argMeta.type !== "Option");
|
|
66
|
+
return [allArgMetas, argMetas, internalArgMetas];
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export interface InternalArgToken<T = unknown, Type extends InternalArgType = InternalArgType> {
|
|
70
|
+
type: Type;
|
|
71
|
+
_value: T;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const createInternalArgToken = <T, Type extends InternalArgType>(type: Type) => ({ type }) as InternalArgToken<T, Type>;
|
|
75
|
+
|
|
76
|
+
export const normalizePrimitiveArgType = (type: PrimitiveArgType): NormalizedPrimitiveArgType => {
|
|
77
|
+
if (type === String) return "string";
|
|
78
|
+
if (type === Number) return "number";
|
|
79
|
+
if (type === Boolean) return "boolean";
|
|
80
|
+
throw new Error(`Invalid primitive argument type: ${type}`);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export const App = createInternalArgToken<AppExecutor, "App">("App");
|
|
84
|
+
export type App = AppExecutor;
|
|
85
|
+
|
|
86
|
+
export const Lib = createInternalArgToken<LibExecutor, "Lib">("Lib");
|
|
87
|
+
export type Lib = LibExecutor;
|
|
88
|
+
|
|
89
|
+
export const Sys = createInternalArgToken<SysExecutor, "Sys">("Sys");
|
|
90
|
+
export type Sys = SysExecutor;
|
|
91
|
+
|
|
92
|
+
export const Exec = createInternalArgToken<Executor, "Exec">("Exec");
|
|
93
|
+
export type Exec = Executor;
|
|
94
|
+
|
|
95
|
+
export const Pkg = createInternalArgToken<PkgExecutor, "Pkg">("Pkg");
|
|
96
|
+
export type Pkg = PkgExecutor;
|
|
97
|
+
|
|
98
|
+
export const Module = createInternalArgToken<ModuleExecutor, "Module">("Module");
|
|
99
|
+
export type Module = ModuleExecutor;
|
|
100
|
+
|
|
101
|
+
export const Workspace = createInternalArgToken<WorkspaceExecutor, "Workspace">("Workspace");
|
|
102
|
+
export type Workspace = WorkspaceExecutor;
|