@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.
Files changed (195) hide show
  1. package/README.ko.md +65 -0
  2. package/README.md +62 -6
  3. package/aiEditor.ts +304 -0
  4. package/akanApp/akanApp.host.ts +393 -0
  5. package/akanApp/index.ts +1 -0
  6. package/akanConfig/akanConfig.test.ts +236 -0
  7. package/akanConfig/akanConfig.ts +384 -0
  8. package/akanConfig/index.ts +2 -0
  9. package/akanConfig/types.ts +23 -0
  10. package/applicationBuildReporter.ts +69 -0
  11. package/applicationBuildRunner.ts +302 -0
  12. package/applicationReleasePackager.ts +206 -0
  13. package/artifact/implicitRootLayout.ts +155 -0
  14. package/artifact/index.ts +1 -0
  15. package/artifact/routeSeedIndex.test.ts +98 -0
  16. package/artifact/routeSeedIndex.ts +130 -0
  17. package/auth.ts +41 -0
  18. package/builder.ts +164 -0
  19. package/capacitor.base.config.ts +88 -0
  20. package/capacitorApp.ts +440 -0
  21. package/commandDecorators/argMeta.ts +102 -0
  22. package/commandDecorators/command.ts +351 -0
  23. package/commandDecorators/commandBuilder.ts +224 -0
  24. package/commandDecorators/commandDecorators.test.ts +212 -0
  25. package/commandDecorators/commandMeta.ts +7 -0
  26. package/commandDecorators/dependencyBuilder.ts +100 -0
  27. package/{esm/src/commandDecorators/helpFormatter.js → commandDecorators/helpFormatter.ts} +100 -47
  28. package/{esm/src/commandDecorators/index.js → commandDecorators/index.ts} +4 -2
  29. package/commandDecorators/targetMeta.ts +31 -0
  30. package/commandDecorators/types.ts +10 -0
  31. package/constants.ts +25 -0
  32. package/createTunnel.ts +36 -0
  33. package/dependencyScanner.ts +357 -0
  34. package/devkitUtils.test.ts +259 -0
  35. package/executors.test.ts +315 -0
  36. package/executors.ts +1390 -0
  37. package/{esm/src/extractDeps.js → extractDeps.ts} +26 -20
  38. package/{esm/src/fileEditor.js → fileEditor.ts} +51 -32
  39. package/fileSys.ts +39 -0
  40. package/frontendBuild/allRoutesBuilder.ts +103 -0
  41. package/frontendBuild/buildRouteClient.test.ts +190 -0
  42. package/frontendBuild/clientBuildTypes.ts +114 -0
  43. package/frontendBuild/clientEntriesBundler.ts +303 -0
  44. package/frontendBuild/clientEntryDiscovery.ts +199 -0
  45. package/frontendBuild/csrArtifactBuilder.ts +237 -0
  46. package/frontendBuild/cssCompiler.ts +286 -0
  47. package/frontendBuild/cssImportResolver.ts +116 -0
  48. package/frontendBuild/fontOptimizer.ts +427 -0
  49. package/frontendBuild/frontendBuild.test.ts +204 -0
  50. package/frontendBuild/hmrChangeClassifier.ts +28 -0
  51. package/frontendBuild/hmrWatcher.ts +102 -0
  52. package/frontendBuild/index.ts +18 -0
  53. package/frontendBuild/pagesBundleBuilder.ts +137 -0
  54. package/frontendBuild/pagesEntrySourceGenerator.ts +37 -0
  55. package/frontendBuild/precompressArtifacts.ts +59 -0
  56. package/frontendBuild/routeClientBuilder.ts +290 -0
  57. package/frontendBuild/routesManifestArtifactSerializer.ts +62 -0
  58. package/frontendBuild/ssrBaseArtifactBuilder.ts +139 -0
  59. package/frontendBuild/vendorSpecifiers.ts +16 -0
  60. package/frontendBuild/watchRootResolver.ts +28 -0
  61. package/getCredentials.ts +19 -0
  62. package/getDirname.ts +3 -0
  63. package/getModelFileData.ts +59 -0
  64. package/getRelatedCnsts.ts +313 -0
  65. package/guideline.ts +19 -0
  66. package/incrementalBuilder/incrementalBuilder.host.test.ts +51 -0
  67. package/incrementalBuilder/incrementalBuilder.host.ts +152 -0
  68. package/incrementalBuilder/incrementalBuilder.proc.ts +331 -0
  69. package/incrementalBuilder/index.ts +1 -0
  70. package/{esm/src/index.js → index.ts} +28 -15
  71. package/lint/no-deep-internal-import.grit +25 -0
  72. package/lint/no-import-client-functions.grit +32 -0
  73. package/lint/no-import-external-library.grit +21 -0
  74. package/lint/no-js-private-class-method.grit +42 -0
  75. package/lint/no-use-client-in-server.grit +7 -0
  76. package/lint/non-scalar-props-restricted.grit +13 -0
  77. package/linter.ts +271 -0
  78. package/mobile/index.ts +1 -0
  79. package/mobile/mobileTarget.test.ts +53 -0
  80. package/mobile/mobileTarget.ts +88 -0
  81. package/package.json +48 -31
  82. package/prompter.ts +72 -0
  83. package/scanInfo.ts +606 -0
  84. package/selectModel.ts +11 -0
  85. package/{esm/src/spinner.js → spinner.ts} +22 -28
  86. package/{esm/src/capacitorApp.js → src/capacitorApp.ts} +82 -81
  87. package/sshTunnel.ts +152 -0
  88. package/{esm/src/streamAi.js → streamAi.ts} +18 -12
  89. package/transforms/barrelAnalyzer.ts +278 -0
  90. package/transforms/barrelImportsPlugin.ts +504 -0
  91. package/transforms/externalizeFrameworkPlugin.ts +185 -0
  92. package/transforms/index.ts +5 -0
  93. package/transforms/rscUseClientTransform.ts +59 -0
  94. package/transforms/transforms.test.ts +208 -0
  95. package/transforms/useClientBundlePlugin.ts +47 -0
  96. package/tsconfig.json +37 -0
  97. package/typeChecker.ts +264 -0
  98. package/types.ts +44 -0
  99. package/ui/MultiScrollList.tsx +242 -0
  100. package/ui/ScrollList.tsx +107 -0
  101. package/ui/index.ts +2 -0
  102. package/{esm/src/uploadRelease.js → uploadRelease.ts} +50 -34
  103. package/{esm/src/useStdoutDimensions.js → useStdoutDimensions.ts} +5 -5
  104. package/cjs/index.js +0 -21
  105. package/cjs/src/aiEditor.js +0 -311
  106. package/cjs/src/auth.js +0 -72
  107. package/cjs/src/builder.js +0 -114
  108. package/cjs/src/capacitorApp.js +0 -313
  109. package/cjs/src/commandDecorators/argMeta.js +0 -88
  110. package/cjs/src/commandDecorators/command.js +0 -324
  111. package/cjs/src/commandDecorators/commandMeta.js +0 -30
  112. package/cjs/src/commandDecorators/helpFormatter.js +0 -211
  113. package/cjs/src/commandDecorators/index.js +0 -31
  114. package/cjs/src/commandDecorators/targetMeta.js +0 -57
  115. package/cjs/src/commandDecorators/types.js +0 -15
  116. package/cjs/src/constants.js +0 -46
  117. package/cjs/src/createTunnel.js +0 -49
  118. package/cjs/src/dependencyScanner.js +0 -220
  119. package/cjs/src/executors.js +0 -964
  120. package/cjs/src/extractDeps.js +0 -103
  121. package/cjs/src/fileEditor.js +0 -120
  122. package/cjs/src/getCredentials.js +0 -44
  123. package/cjs/src/getDirname.js +0 -38
  124. package/cjs/src/getModelFileData.js +0 -66
  125. package/cjs/src/getRelatedCnsts.js +0 -260
  126. package/cjs/src/guideline.js +0 -15
  127. package/cjs/src/index.js +0 -65
  128. package/cjs/src/linter.js +0 -238
  129. package/cjs/src/prompter.js +0 -85
  130. package/cjs/src/scanInfo.js +0 -491
  131. package/cjs/src/selectModel.js +0 -46
  132. package/cjs/src/spinner.js +0 -93
  133. package/cjs/src/streamAi.js +0 -62
  134. package/cjs/src/typeChecker.js +0 -207
  135. package/cjs/src/types.js +0 -15
  136. package/cjs/src/uploadRelease.js +0 -112
  137. package/cjs/src/useStdoutDimensions.js +0 -43
  138. package/esm/index.js +0 -1
  139. package/esm/src/aiEditor.js +0 -282
  140. package/esm/src/auth.js +0 -42
  141. package/esm/src/builder.js +0 -81
  142. package/esm/src/commandDecorators/argMeta.js +0 -54
  143. package/esm/src/commandDecorators/command.js +0 -290
  144. package/esm/src/commandDecorators/commandMeta.js +0 -7
  145. package/esm/src/commandDecorators/targetMeta.js +0 -33
  146. package/esm/src/commandDecorators/types.js +0 -0
  147. package/esm/src/constants.js +0 -17
  148. package/esm/src/createTunnel.js +0 -26
  149. package/esm/src/dependencyScanner.js +0 -187
  150. package/esm/src/executors.js +0 -928
  151. package/esm/src/getCredentials.js +0 -11
  152. package/esm/src/getDirname.js +0 -5
  153. package/esm/src/getModelFileData.js +0 -33
  154. package/esm/src/getRelatedCnsts.js +0 -221
  155. package/esm/src/guideline.js +0 -0
  156. package/esm/src/linter.js +0 -205
  157. package/esm/src/prompter.js +0 -51
  158. package/esm/src/scanInfo.js +0 -455
  159. package/esm/src/selectModel.js +0 -13
  160. package/esm/src/typeChecker.js +0 -174
  161. package/esm/src/types.js +0 -0
  162. package/index.d.ts +0 -1
  163. package/src/aiEditor.d.ts +0 -50
  164. package/src/auth.d.ts +0 -9
  165. package/src/builder.d.ts +0 -18
  166. package/src/capacitorApp.d.ts +0 -39
  167. package/src/commandDecorators/argMeta.d.ts +0 -67
  168. package/src/commandDecorators/command.d.ts +0 -2
  169. package/src/commandDecorators/commandMeta.d.ts +0 -2
  170. package/src/commandDecorators/helpFormatter.d.ts +0 -3
  171. package/src/commandDecorators/index.d.ts +0 -6
  172. package/src/commandDecorators/targetMeta.d.ts +0 -19
  173. package/src/commandDecorators/types.d.ts +0 -1
  174. package/src/constants.d.ts +0 -26
  175. package/src/createTunnel.d.ts +0 -8
  176. package/src/dependencyScanner.d.ts +0 -23
  177. package/src/executors.d.ts +0 -296
  178. package/src/extractDeps.d.ts +0 -7
  179. package/src/fileEditor.d.ts +0 -16
  180. package/src/getCredentials.d.ts +0 -12
  181. package/src/getDirname.d.ts +0 -1
  182. package/src/getModelFileData.d.ts +0 -16
  183. package/src/getRelatedCnsts.d.ts +0 -53
  184. package/src/guideline.d.ts +0 -19
  185. package/src/index.d.ts +0 -23
  186. package/src/linter.d.ts +0 -109
  187. package/src/prompter.d.ts +0 -14
  188. package/src/scanInfo.d.ts +0 -82
  189. package/src/selectModel.d.ts +0 -1
  190. package/src/spinner.d.ts +0 -20
  191. package/src/streamAi.d.ts +0 -6
  192. package/src/typeChecker.d.ts +0 -52
  193. package/src/types.d.ts +0 -31
  194. package/src/uploadRelease.d.ts +0 -10
  195. package/src/useStdoutDimensions.d.ts +0 -1
@@ -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;