@akanjs/devkit 1.0.19 → 2.1.0-rc.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.
Files changed (194) hide show
  1. package/aiEditor.ts +304 -0
  2. package/akanApp/akanApp.host.ts +393 -0
  3. package/akanApp/index.ts +1 -0
  4. package/akanConfig/akanConfig.test.ts +236 -0
  5. package/akanConfig/akanConfig.ts +384 -0
  6. package/akanConfig/index.ts +2 -0
  7. package/akanConfig/types.ts +23 -0
  8. package/applicationBuildReporter.ts +69 -0
  9. package/applicationBuildRunner.ts +302 -0
  10. package/applicationReleasePackager.ts +206 -0
  11. package/artifact/implicitRootLayout.ts +155 -0
  12. package/artifact/index.ts +1 -0
  13. package/artifact/routeSeedIndex.test.ts +98 -0
  14. package/artifact/routeSeedIndex.ts +130 -0
  15. package/auth.ts +41 -0
  16. package/builder.ts +164 -0
  17. package/capacitor.base.config.ts +88 -0
  18. package/capacitorApp.ts +440 -0
  19. package/commandDecorators/argMeta.ts +102 -0
  20. package/commandDecorators/command.ts +343 -0
  21. package/commandDecorators/commandBuilder.ts +224 -0
  22. package/commandDecorators/commandDecorators.test.ts +212 -0
  23. package/commandDecorators/commandMeta.ts +7 -0
  24. package/commandDecorators/dependencyBuilder.ts +100 -0
  25. package/{esm/src/commandDecorators/helpFormatter.js → commandDecorators/helpFormatter.ts} +100 -47
  26. package/{esm/src/commandDecorators/index.js → commandDecorators/index.ts} +4 -2
  27. package/commandDecorators/targetMeta.ts +31 -0
  28. package/commandDecorators/types.ts +10 -0
  29. package/constants.ts +25 -0
  30. package/createTunnel.ts +36 -0
  31. package/dependencyScanner.ts +357 -0
  32. package/devkitUtils.test.ts +259 -0
  33. package/executors.test.ts +315 -0
  34. package/executors.ts +1390 -0
  35. package/{esm/src/extractDeps.js → extractDeps.ts} +26 -20
  36. package/{esm/src/fileEditor.js → fileEditor.ts} +51 -32
  37. package/fileSys.ts +39 -0
  38. package/frontendBuild/allRoutesBuilder.ts +103 -0
  39. package/frontendBuild/buildRouteClient.test.ts +190 -0
  40. package/frontendBuild/clientBuildTypes.ts +114 -0
  41. package/frontendBuild/clientEntriesBundler.ts +303 -0
  42. package/frontendBuild/clientEntryDiscovery.ts +199 -0
  43. package/frontendBuild/csrArtifactBuilder.ts +237 -0
  44. package/frontendBuild/cssCompiler.ts +286 -0
  45. package/frontendBuild/cssImportResolver.ts +116 -0
  46. package/frontendBuild/fontOptimizer.ts +427 -0
  47. package/frontendBuild/frontendBuild.test.ts +204 -0
  48. package/frontendBuild/hmrChangeClassifier.ts +28 -0
  49. package/frontendBuild/hmrWatcher.ts +102 -0
  50. package/frontendBuild/index.ts +18 -0
  51. package/frontendBuild/pagesBundleBuilder.ts +137 -0
  52. package/frontendBuild/pagesEntrySourceGenerator.ts +37 -0
  53. package/frontendBuild/precompressArtifacts.ts +59 -0
  54. package/frontendBuild/routeClientBuilder.ts +290 -0
  55. package/frontendBuild/routesManifestArtifactSerializer.ts +62 -0
  56. package/frontendBuild/ssrBaseArtifactBuilder.ts +139 -0
  57. package/frontendBuild/vendorSpecifiers.ts +16 -0
  58. package/frontendBuild/watchRootResolver.ts +28 -0
  59. package/getCredentials.ts +19 -0
  60. package/getDirname.ts +3 -0
  61. package/getModelFileData.ts +59 -0
  62. package/getRelatedCnsts.ts +313 -0
  63. package/guideline.ts +19 -0
  64. package/incrementalBuilder/incrementalBuilder.host.test.ts +51 -0
  65. package/incrementalBuilder/incrementalBuilder.host.ts +152 -0
  66. package/incrementalBuilder/incrementalBuilder.proc.ts +331 -0
  67. package/incrementalBuilder/index.ts +1 -0
  68. package/{esm/src/index.js → index.ts} +28 -15
  69. package/lint/no-deep-internal-import.grit +25 -0
  70. package/lint/no-import-client-functions.grit +32 -0
  71. package/lint/no-import-external-library.grit +21 -0
  72. package/lint/no-js-private-class-method.grit +42 -0
  73. package/lint/no-use-client-in-server.grit +7 -0
  74. package/lint/non-scalar-props-restricted.grit +13 -0
  75. package/linter.ts +271 -0
  76. package/mobile/index.ts +1 -0
  77. package/mobile/mobileTarget.test.ts +53 -0
  78. package/mobile/mobileTarget.ts +88 -0
  79. package/package.json +48 -31
  80. package/prompter.ts +72 -0
  81. package/scanInfo.ts +606 -0
  82. package/selectModel.ts +11 -0
  83. package/{esm/src/spinner.js → spinner.ts} +22 -28
  84. package/{esm/src/capacitorApp.js → src/capacitorApp.ts} +82 -81
  85. package/sshTunnel.ts +152 -0
  86. package/{esm/src/streamAi.js → streamAi.ts} +18 -12
  87. package/transforms/barrelAnalyzer.ts +278 -0
  88. package/transforms/barrelImportsPlugin.ts +504 -0
  89. package/transforms/externalizeFrameworkPlugin.ts +185 -0
  90. package/transforms/index.ts +5 -0
  91. package/transforms/rscUseClientTransform.ts +59 -0
  92. package/transforms/transforms.test.ts +208 -0
  93. package/transforms/useClientBundlePlugin.ts +47 -0
  94. package/tsconfig.json +37 -0
  95. package/typeChecker.ts +264 -0
  96. package/types.ts +44 -0
  97. package/ui/MultiScrollList.tsx +242 -0
  98. package/ui/ScrollList.tsx +107 -0
  99. package/ui/index.ts +2 -0
  100. package/{esm/src/uploadRelease.js → uploadRelease.ts} +50 -34
  101. package/{esm/src/useStdoutDimensions.js → useStdoutDimensions.ts} +5 -5
  102. package/README.md +0 -1
  103. package/cjs/index.js +0 -21
  104. package/cjs/src/aiEditor.js +0 -311
  105. package/cjs/src/auth.js +0 -72
  106. package/cjs/src/builder.js +0 -114
  107. package/cjs/src/capacitorApp.js +0 -313
  108. package/cjs/src/commandDecorators/argMeta.js +0 -88
  109. package/cjs/src/commandDecorators/command.js +0 -324
  110. package/cjs/src/commandDecorators/commandMeta.js +0 -30
  111. package/cjs/src/commandDecorators/helpFormatter.js +0 -211
  112. package/cjs/src/commandDecorators/index.js +0 -31
  113. package/cjs/src/commandDecorators/targetMeta.js +0 -57
  114. package/cjs/src/commandDecorators/types.js +0 -15
  115. package/cjs/src/constants.js +0 -46
  116. package/cjs/src/createTunnel.js +0 -49
  117. package/cjs/src/dependencyScanner.js +0 -220
  118. package/cjs/src/executors.js +0 -964
  119. package/cjs/src/extractDeps.js +0 -103
  120. package/cjs/src/fileEditor.js +0 -120
  121. package/cjs/src/getCredentials.js +0 -44
  122. package/cjs/src/getDirname.js +0 -38
  123. package/cjs/src/getModelFileData.js +0 -66
  124. package/cjs/src/getRelatedCnsts.js +0 -260
  125. package/cjs/src/guideline.js +0 -15
  126. package/cjs/src/index.js +0 -65
  127. package/cjs/src/linter.js +0 -238
  128. package/cjs/src/prompter.js +0 -85
  129. package/cjs/src/scanInfo.js +0 -491
  130. package/cjs/src/selectModel.js +0 -46
  131. package/cjs/src/spinner.js +0 -93
  132. package/cjs/src/streamAi.js +0 -62
  133. package/cjs/src/typeChecker.js +0 -207
  134. package/cjs/src/types.js +0 -15
  135. package/cjs/src/uploadRelease.js +0 -112
  136. package/cjs/src/useStdoutDimensions.js +0 -43
  137. package/esm/index.js +0 -1
  138. package/esm/src/aiEditor.js +0 -282
  139. package/esm/src/auth.js +0 -42
  140. package/esm/src/builder.js +0 -81
  141. package/esm/src/commandDecorators/argMeta.js +0 -54
  142. package/esm/src/commandDecorators/command.js +0 -290
  143. package/esm/src/commandDecorators/commandMeta.js +0 -7
  144. package/esm/src/commandDecorators/targetMeta.js +0 -33
  145. package/esm/src/commandDecorators/types.js +0 -0
  146. package/esm/src/constants.js +0 -17
  147. package/esm/src/createTunnel.js +0 -26
  148. package/esm/src/dependencyScanner.js +0 -187
  149. package/esm/src/executors.js +0 -928
  150. package/esm/src/getCredentials.js +0 -11
  151. package/esm/src/getDirname.js +0 -5
  152. package/esm/src/getModelFileData.js +0 -33
  153. package/esm/src/getRelatedCnsts.js +0 -221
  154. package/esm/src/guideline.js +0 -0
  155. package/esm/src/linter.js +0 -205
  156. package/esm/src/prompter.js +0 -51
  157. package/esm/src/scanInfo.js +0 -455
  158. package/esm/src/selectModel.js +0 -13
  159. package/esm/src/typeChecker.js +0 -174
  160. package/esm/src/types.js +0 -0
  161. package/index.d.ts +0 -1
  162. package/src/aiEditor.d.ts +0 -50
  163. package/src/auth.d.ts +0 -9
  164. package/src/builder.d.ts +0 -18
  165. package/src/capacitorApp.d.ts +0 -39
  166. package/src/commandDecorators/argMeta.d.ts +0 -67
  167. package/src/commandDecorators/command.d.ts +0 -2
  168. package/src/commandDecorators/commandMeta.d.ts +0 -2
  169. package/src/commandDecorators/helpFormatter.d.ts +0 -3
  170. package/src/commandDecorators/index.d.ts +0 -6
  171. package/src/commandDecorators/targetMeta.d.ts +0 -19
  172. package/src/commandDecorators/types.d.ts +0 -1
  173. package/src/constants.d.ts +0 -26
  174. package/src/createTunnel.d.ts +0 -8
  175. package/src/dependencyScanner.d.ts +0 -23
  176. package/src/executors.d.ts +0 -296
  177. package/src/extractDeps.d.ts +0 -7
  178. package/src/fileEditor.d.ts +0 -16
  179. package/src/getCredentials.d.ts +0 -12
  180. package/src/getDirname.d.ts +0 -1
  181. package/src/getModelFileData.d.ts +0 -16
  182. package/src/getRelatedCnsts.d.ts +0 -53
  183. package/src/guideline.d.ts +0 -19
  184. package/src/index.d.ts +0 -23
  185. package/src/linter.d.ts +0 -109
  186. package/src/prompter.d.ts +0 -14
  187. package/src/scanInfo.d.ts +0 -82
  188. package/src/selectModel.d.ts +0 -1
  189. package/src/spinner.d.ts +0 -20
  190. package/src/streamAi.d.ts +0 -6
  191. package/src/typeChecker.d.ts +0 -52
  192. package/src/types.d.ts +0 -31
  193. package/src/uploadRelease.d.ts +0 -10
  194. package/src/useStdoutDimensions.d.ts +0 -1
@@ -0,0 +1,236 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import type { PackageJson } from "../types";
6
+ import { AkanAppConfig, AkanLibConfig } from "./akanConfig";
7
+ import type { DeepPartial, LibConfigResult } from "./types";
8
+
9
+ const akanPackageJson = JSON.parse(
10
+ fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), "../../../akanjs/package.json"), "utf8"),
11
+ ) as PackageJson;
12
+
13
+ const packageJson: PackageJson = {
14
+ name: "repo",
15
+ version: "1.0.0",
16
+ description: "repo",
17
+ dependencies: {
18
+ react: "19.0.0",
19
+ "react-dom": "19.0.0",
20
+ "react-server-dom-webpack": "19.0.0",
21
+ sharp: "1.0.0",
22
+ "@external/runtime": "2.0.0",
23
+ },
24
+ };
25
+
26
+ const app = { name: "portal" } as never;
27
+ const baseDevEnv = {
28
+ repoName: "akansoft",
29
+ serveDomain: "akamir.com",
30
+ env: "debug" as const,
31
+ portOffset: 0,
32
+ workspaceRoot: "/workspace",
33
+ };
34
+
35
+ describe("AkanAppConfig", () => {
36
+ test("applies defaults for route domains, i18n, image, mobile, and imports", () => {
37
+ const config = new AkanAppConfig(app, ["shared"], packageJson, {}, baseDevEnv);
38
+
39
+ expect([...config.domains].sort()).toEqual([
40
+ "portal-debug.akamir.com",
41
+ "portal-develop.akamir.com",
42
+ "portal-main.akamir.com",
43
+ ]);
44
+ expect(config.basePaths.size).toBe(0);
45
+ expect(config.i18n.defaultLocale).toBe("en");
46
+ expect(config.i18n.locales).toContain("en");
47
+ expect(config.images.formats).toEqual(["image/webp"]);
48
+ expect(config.mobile).toMatchObject({
49
+ appName: "portal",
50
+ appId: "com.portal.app",
51
+ version: "0.0.1",
52
+ buildNum: 1,
53
+ targets: {
54
+ default: {
55
+ name: "default",
56
+ appName: "portal",
57
+ appId: "com.portal.app",
58
+ version: "0.0.1",
59
+ buildNum: 1,
60
+ },
61
+ },
62
+ });
63
+ expect(config.barrelImports).toEqual(
64
+ expect.arrayContaining(["@apps/portal/ui", "@libs/shared/server", "akanjs/common"]),
65
+ );
66
+ expect(config.docker.content).toContain("ENV AKAN_PUBLIC_APP_NAME=portal");
67
+ expect(process.env.AKAN_PUBLIC_DEFAULT_LOCALE).toBe("en");
68
+ });
69
+
70
+ test("normalizes explicit routes, branch domains, base paths, and docker options", () => {
71
+ const config = new AkanAppConfig(
72
+ app,
73
+ [],
74
+ packageJson,
75
+ {
76
+ routes: [
77
+ { domains: { debug: ["Root.Local:8282"], qa: ["QA.Root.Local"] } },
78
+ { basePath: "/admin/", domains: { debug: ["Admin.Local:8282"], main: ["Admin.Main.Local"] } },
79
+ ],
80
+ i18n: { locales: ["ko", "en"], defaultLocale: "ko" },
81
+ mobile: { appName: "Portal App", appId: "com.portal.mobile", version: "1.2.3", buildNum: 7 },
82
+ images: { qualities: [80, 90], dangerouslyAllowSVG: true },
83
+ docker: {
84
+ image: { amd64: "oven/bun:amd64", arm64: "oven/bun:arm64" },
85
+ preRuns: ["echo before", { arm64: "echo arm" }],
86
+ postRuns: ["echo after"],
87
+ command: ["bun", "server.js"],
88
+ },
89
+ optimizeImports: ["custom-icons"],
90
+ publicEnv: ["AKAN_PUBLIC_FEATURE"],
91
+ },
92
+ baseDevEnv,
93
+ );
94
+
95
+ expect([...config.domains].sort()).toEqual(["qa.root.local", "root.local"]);
96
+ expect([...config.basePaths]).toEqual(["admin"]);
97
+ expect([...(config.subRoutes.get("admin") ?? [])].sort()).toEqual([
98
+ "admin-debug.akamir.com",
99
+ "admin-develop.akamir.com",
100
+ "admin-main.akamir.com",
101
+ "admin-qa.akamir.com",
102
+ "admin.local",
103
+ "admin.main.local",
104
+ ]);
105
+ expect([...config.branches].sort()).toEqual(["debug", "develop", "main", "qa"]);
106
+ expect(config.i18n.defaultLocale).toBe("ko");
107
+ expect(config.images.qualities).toEqual([80, 90]);
108
+ expect(config.images.dangerouslyAllowSVG).toBe(true);
109
+ expect(config.mobile.buildNum).toBe(7);
110
+ expect(config.mobile.targets.default).toMatchObject({
111
+ name: "default",
112
+ appName: "Portal App",
113
+ appId: "com.portal.mobile",
114
+ version: "1.2.3",
115
+ buildNum: 7,
116
+ });
117
+ expect(config.publicEnv).toEqual(["AKAN_PUBLIC_FEATURE"]);
118
+ expect(config.optimizeImports).toContain("custom-icons");
119
+ expect(config.docker.content).toContain('CMD ["bun","server.js"]');
120
+ expect(config.docker.content).toContain("FROM oven/bun:amd64 AS amd64");
121
+ expect(config.docker.content).toContain('RUN if [ "$TARGETARCH" = "arm64"');
122
+ });
123
+
124
+ test("creates production package json and reports missing external versions", () => {
125
+ const config = new AkanAppConfig(app, [], packageJson, { externalLibs: ["@external/runtime"] }, baseDevEnv);
126
+
127
+ expect(config.getProductionPackageJson({ scripts: { start: "bun main.js" } })).toMatchObject({
128
+ name: "portal",
129
+ main: "./main.js",
130
+ scripts: { start: "bun main.js" },
131
+ dependencies: {
132
+ react: "19.0.0",
133
+ "react-dom": "19.0.0",
134
+ "react-server-dom-webpack": "19.0.0",
135
+ sharp: "1.0.0",
136
+ "@external/runtime": "2.0.0",
137
+ },
138
+ });
139
+
140
+ const brokenConfig = new AkanAppConfig(
141
+ app,
142
+ [],
143
+ { ...packageJson, dependencies: { react: "19.0.0" } },
144
+ { externalLibs: ["missing-lib"] },
145
+ baseDevEnv,
146
+ );
147
+ expect(() => brokenConfig.getProductionPackageJson()).toThrow("Dependency missing-lib not found");
148
+ });
149
+
150
+ test("falls back to akanjs package versions for built-in runtime dependencies", () => {
151
+ const runtimeDependencies = { ...akanPackageJson.dependencies, ...akanPackageJson.peerDependencies };
152
+ const config = new AkanAppConfig(
153
+ app,
154
+ [],
155
+ {
156
+ name: "repo",
157
+ version: "1.0.0",
158
+ description: "repo",
159
+ dependencies: {
160
+ akanjs: "2.0.5-canary.0",
161
+ },
162
+ },
163
+ {},
164
+ baseDevEnv,
165
+ );
166
+
167
+ expect(config.getProductionPackageJson().dependencies).toEqual({
168
+ react: runtimeDependencies.react,
169
+ "react-dom": runtimeDependencies["react-dom"],
170
+ "react-server-dom-webpack": runtimeDependencies["react-server-dom-webpack"],
171
+ sharp: runtimeDependencies.sharp,
172
+ });
173
+ });
174
+
175
+ test("normalizes multiple mobile targets and validates base paths", () => {
176
+ const config = new AkanAppConfig(
177
+ app,
178
+ [],
179
+ packageJson,
180
+ {
181
+ routes: [{ basePath: "admin", domains: {} }],
182
+ mobile: {
183
+ appName: "Portal",
184
+ appId: "com.portal.app",
185
+ version: "1.0.0",
186
+ buildNum: 3,
187
+ targets: {
188
+ admin: {
189
+ basePath: "admin",
190
+ appName: "Portal Admin",
191
+ appId: "com.portal.admin",
192
+ buildNum: 8,
193
+ permissions: ["camera"],
194
+ links: { schemes: ["portal-admin"] },
195
+ },
196
+ },
197
+ },
198
+ },
199
+ baseDevEnv,
200
+ );
201
+
202
+ expect(config.mobile.targets.admin).toMatchObject({
203
+ name: "admin",
204
+ basePath: "admin",
205
+ appName: "Portal Admin",
206
+ appId: "com.portal.admin",
207
+ version: "1.0.0",
208
+ buildNum: 8,
209
+ permissions: ["camera"],
210
+ links: { schemes: ["portal-admin"] },
211
+ });
212
+
213
+ expect(
214
+ () =>
215
+ new AkanAppConfig(
216
+ app,
217
+ [],
218
+ packageJson,
219
+ {
220
+ mobile: { targets: { bad: { basePath: "missing" } } },
221
+ },
222
+ baseDevEnv,
223
+ ),
224
+ ).toThrow("unknown basePath");
225
+ });
226
+ });
227
+
228
+ describe("AkanLibConfig", () => {
229
+ test("uses empty external libs by default and preserves explicit libs", () => {
230
+ const lib = { name: "shared" } as never;
231
+ expect(new AkanLibConfig(lib, {}).externalLibs).toEqual([]);
232
+
233
+ const config: DeepPartial<LibConfigResult> = { externalLibs: ["firebase-admin"] };
234
+ expect(new AkanLibConfig(lib, config).externalLibs).toEqual(["firebase-admin"]);
235
+ });
236
+ });
@@ -0,0 +1,384 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { type AkanI18nConfig, resolveAkanI18nConfig } from "akanjs/common";
5
+ import type { AkanImageConfig } from "akanjs/server";
6
+ import type { App, Lib } from "../commandDecorators";
7
+ import { WorkspaceExecutor } from "../executors";
8
+ import type { BaseDevEnv, PackageJson } from "../types";
9
+ import {
10
+ type AkanMobileConfig,
11
+ type AkanMobileTargetConfig,
12
+ type AkanRouteConfig,
13
+ type AppConfigResult,
14
+ type Arch,
15
+ archs,
16
+ type DatabaseMode,
17
+ type DeepPartial,
18
+ type DockerConfig,
19
+ type LibConfigResult,
20
+ } from "./types";
21
+
22
+ const DEFAULT_BARREL_IMPORTS = ["akanjs/webkit", "akanjs/common", "akanjs/ui"];
23
+ const DEFAULT_OPTIMIZE_IMPORTS = [
24
+ "lucide-react",
25
+ "date-fns",
26
+ "lodash-es",
27
+ "ramda",
28
+ "antd",
29
+ "react-bootstrap",
30
+ "ahooks",
31
+ "@ant-design/icons",
32
+ "@headlessui/react",
33
+ "@headlessui-float/react",
34
+ "@heroicons/react/20/solid",
35
+ "@heroicons/react/24/solid",
36
+ "@heroicons/react/24/outline",
37
+ "@visx/visx",
38
+ "@tremor/react",
39
+ "rxjs",
40
+ "@mui/material",
41
+ "@mui/icons-material",
42
+ "recharts",
43
+ "react-use",
44
+ "@material-ui/core",
45
+ "@material-ui/icons",
46
+ "@tabler/icons-react",
47
+ "mui-core",
48
+ "react-icons/*",
49
+ ];
50
+ const WORKSPACE_BARREL_FACETS = ["ui", "webkit", "common", "client", "server"] as const;
51
+ const SSR_RUNTIME_PACKAGES = ["react", "react-dom", "react-server-dom-webpack"] as const;
52
+ const NATIVE_RUNTIME_PACKAGES = ["sharp"] as const;
53
+ const AKAN_RUNTIME_PACKAGES = new Set<string>([...SSR_RUNTIME_PACKAGES, ...NATIVE_RUNTIME_PACKAGES]);
54
+ const DEFAULT_AKAN_IMAGE_CONFIG: AkanImageConfig = {
55
+ deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
56
+ imageSizes: [32, 48, 64, 96, 128, 256, 384],
57
+ formats: ["image/webp"],
58
+ qualities: [75],
59
+ minimumCacheTTL: 14400,
60
+ remotePatterns: [],
61
+ localPatterns: [{ pathname: "/**" }],
62
+ dangerouslyAllowSVG: false,
63
+ maximumRedirects: 3,
64
+ fetchTimeoutMs: 7000,
65
+ maxRemoteBytes: 25 * 1024 * 1024,
66
+ };
67
+
68
+ export class AkanAppConfig implements AppConfigResult {
69
+ app: App;
70
+ rootPackageJson: PackageJson;
71
+ docker: DockerConfig;
72
+ defaultDatabaseMode: DatabaseMode;
73
+ externalLibs: string[];
74
+ barrelImports: string[];
75
+ optimizeImports: string[];
76
+ images: AkanImageConfig;
77
+ i18n: AkanI18nConfig;
78
+ publicEnv: string[];
79
+ mobile: AkanMobileConfig;
80
+ baseDevEnv: BaseDevEnv;
81
+ libs: string[];
82
+ domains = new Set<string>();
83
+ subRoutes = new Map<string, Set<string>>();
84
+ basePaths = new Set<string>();
85
+ branches = new Set<string>(["debug", "develop", "main"]);
86
+ constructor(
87
+ app: App,
88
+ libs: string[],
89
+ rootPackageJson: PackageJson,
90
+ config: DeepPartial<AppConfigResult>,
91
+ baseDevEnv: BaseDevEnv,
92
+ ) {
93
+ this.app = app;
94
+ this.rootPackageJson = rootPackageJson;
95
+ this.libs = libs;
96
+ this.baseDevEnv = baseDevEnv;
97
+ this.#applyRoutes(config?.routes);
98
+ this.defaultDatabaseMode = config?.defaultDatabaseMode ?? "single";
99
+ this.externalLibs = config?.externalLibs ?? [];
100
+ this.barrelImports = [
101
+ ...DEFAULT_BARREL_IMPORTS,
102
+ ...WORKSPACE_BARREL_FACETS.map((facet) => `@apps/${app.name}/${facet}`),
103
+ ...libs.flatMap((lib) => WORKSPACE_BARREL_FACETS.map((facet) => `@libs/${lib}/${facet}`)),
104
+ ...(config?.barrelImports ?? []),
105
+ ];
106
+ this.optimizeImports = [...new Set([...DEFAULT_OPTIMIZE_IMPORTS, ...(config?.optimizeImports ?? [])])];
107
+ this.images = mergeImageConfig(config?.images as Partial<AkanImageConfig> | undefined);
108
+ this.i18n = resolveAkanI18nConfig(config?.i18n);
109
+ process.env.AKAN_PUBLIC_DEFAULT_LOCALE = this.i18n.defaultLocale;
110
+ process.env.AKAN_PUBLIC_LOCALES = this.i18n.locales.join(",");
111
+ this.publicEnv = (config?.publicEnv as string[] | undefined) ?? ([] as string[]);
112
+ this.mobile = this.#resolveMobileConfig(config.mobile);
113
+ this.docker = this.#makeDockerContent(config?.docker ?? {});
114
+ }
115
+ #resolveMobileConfig(mobile: DeepPartial<AkanMobileConfig> | undefined): AkanMobileConfig {
116
+ const { targets: rawTargets, ...rawMobile } = mobile ?? {};
117
+ const appName = rawMobile.appName ?? this.app.name;
118
+ const appId = rawMobile.appId ?? `com.${this.app.name}.app`;
119
+ const version = rawMobile.version ?? "0.0.1";
120
+ const buildNum = rawMobile.buildNum ?? 1;
121
+ const defaultTargetName = this.#defaultMobileTargetName(rawTargets);
122
+ const targetEntries = Object.entries(
123
+ rawTargets ?? {
124
+ [defaultTargetName]: {},
125
+ },
126
+ );
127
+ const targets = Object.fromEntries(
128
+ targetEntries.map(([name, rawTarget]) => {
129
+ const target = rawTarget as DeepPartial<AkanMobileTargetConfig>;
130
+ const fallbackBasePath = !rawTargets && this.basePaths.has(name) ? name : undefined;
131
+ const basePath = (target.basePath ?? fallbackBasePath)?.replace(/^\/+|\/+$/g, "") || undefined;
132
+ if (basePath && !this.basePaths.has(basePath)) {
133
+ throw new Error(
134
+ `Mobile target '${name}' uses unknown basePath '${basePath}' in apps/${this.app.name}/akan.config.ts`,
135
+ );
136
+ }
137
+ const resolved = {
138
+ ...rawMobile,
139
+ ...target,
140
+ name,
141
+ basePath,
142
+ appName: target.appName ?? appName,
143
+ appId: target.appId ?? appId,
144
+ version: target.version ?? version,
145
+ buildNum: target.buildNum ?? buildNum,
146
+ plugins: {
147
+ ...rawMobile.plugins,
148
+ ...target.plugins,
149
+ },
150
+ android: {
151
+ ...rawMobile.android,
152
+ ...target.android,
153
+ },
154
+ ios: {
155
+ ...rawMobile.ios,
156
+ ...target.ios,
157
+ },
158
+ } satisfies AkanMobileTargetConfig;
159
+ return [name, resolved];
160
+ }),
161
+ );
162
+ return {
163
+ ...rawMobile,
164
+ appName,
165
+ appId,
166
+ version,
167
+ buildNum,
168
+ targets,
169
+ plugins: rawMobile.plugins,
170
+ } as AkanMobileConfig;
171
+ }
172
+ #defaultMobileTargetName(rawTargets: DeepPartial<AkanMobileConfig>["targets"] | undefined) {
173
+ if (rawTargets && Object.keys(rawTargets).length > 0) return Object.keys(rawTargets)[0] as string;
174
+ return this.basePaths.has(this.app.name) ? this.app.name : "default";
175
+ }
176
+ #applyRoutes(routes: AkanRouteConfig[] = []) {
177
+ for (const route of routes) {
178
+ if (route.basePath) {
179
+ const basePath = route.basePath.replace(/^\/+|\/+$/g, "");
180
+ this.basePaths.add(basePath);
181
+ const domains = this.subRoutes.getOrInsert(basePath, new Set());
182
+ Object.keys(route.domains).forEach((branch) => void this.branches.add(branch));
183
+ Object.values(route.domains)
184
+ .flat()
185
+ .forEach((domain) => {
186
+ if (domain) domains.add(domain.toLowerCase().replace(/:\d+$/, ""));
187
+ });
188
+ } else {
189
+ Object.keys(route.domains).forEach((branch) => void this.branches.add(branch));
190
+ Object.values(route.domains)
191
+ .flat()
192
+ .forEach((domain) => {
193
+ if (domain) this.domains.add(domain.toLowerCase().replace(/:\d+$/, ""));
194
+ });
195
+ }
196
+ }
197
+ const appName = this.app.name.toLowerCase();
198
+ const serveDomain = this.baseDevEnv.serveDomain.toLowerCase();
199
+ if (this.subRoutes.size === 0)
200
+ this.branches.forEach((branch) => void this.domains.add(`${appName}-${branch}.${serveDomain}`));
201
+ else
202
+ Array.from(this.subRoutes.entries()).forEach(([basePath, domains]) => {
203
+ this.branches.forEach((domain) => void domains.add(`${basePath}-${domain}.${serveDomain}`));
204
+ });
205
+ }
206
+ #getDockerRunScripts(runs: (string | { [key in Arch]?: string })[]) {
207
+ return runs.map((run) => {
208
+ if (typeof run === "string") return `RUN ${run}`;
209
+ else
210
+ return Object.entries(run)
211
+ .map(
212
+ ([arch, script]) => `RUN if [ "$TARGETARCH" = "${arch}" ]; then \
213
+ ${script}; \
214
+ fi`,
215
+ )
216
+ .join("\n");
217
+ });
218
+ }
219
+ #getDockerImageScript(image: string | { [key in Arch]?: string }, defaultImage: string) {
220
+ if (typeof image === "string") return `FROM ${image}`;
221
+ else return archs.map((arch) => `FROM ${image[arch] ?? defaultImage} AS ${arch}`).join("\n");
222
+ }
223
+ #makeDockerContent(docker: DeepPartial<DockerConfig>): DockerConfig {
224
+ if (docker.content) return { content: docker.content, image: {}, preRuns: [], postRuns: [], command: [] };
225
+ const preRunScripts = this.#getDockerRunScripts(docker.preRuns ?? []);
226
+ const postRunScripts = this.#getDockerRunScripts(docker.postRuns ?? []);
227
+
228
+ const imageScript = docker.image
229
+ ? this.#getDockerImageScript(docker.image, "oven/bun:1-slim")
230
+ : "FROM oven/bun:1-slim";
231
+ const command = docker.command ?? ["bun", "main.js"];
232
+ const content = `${imageScript}
233
+ RUN ln -sf /usr/share/zoneinfo/Asia/Seoul /etc/localtime
234
+ RUN apt-get update && apt-get upgrade -y
235
+ RUN apt-get install -y --no-install-recommends git redis build-essential python3 ca-certificates fonts-liberation libappindicator3-1 libasound2 libatk-bridge2.0-0 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgbm1 libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 libnss3 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 lsb-release wget xdg-utils udev ffmpeg
236
+ ARG TARGETARCH
237
+ ${preRunScripts.join("\n")}
238
+ RUN mkdir -p /workspace
239
+ WORKDIR /workspace
240
+ COPY ./package.json ./package.json
241
+ RUN bun install --production
242
+ ${postRunScripts.join("\n")}
243
+ COPY . .
244
+ ENV PORT=8282
245
+ ENV NODE_ENV=production
246
+ ENV AKAN_PUBLIC_REPO_NAME=${this.baseDevEnv.repoName}
247
+ ENV AKAN_PUBLIC_SERVE_DOMAIN=${this.baseDevEnv.serveDomain}
248
+ ENV AKAN_PUBLIC_APP_NAME=${this.app.name}
249
+ ENV AKAN_PUBLIC_ENV=${this.baseDevEnv.env}
250
+ ${this.basePaths.size ? `ENV AKAN_PUBLIC_BASE_PATHS=${[...this.basePaths].join(",")}` : ""}
251
+ ENV AKAN_PUBLIC_DEFAULT_LOCALE=${this.i18n.defaultLocale}
252
+ ENV AKAN_PUBLIC_LOCALES=${this.i18n.locales.join(",")}
253
+ ENV AKAN_PUBLIC_OPERATION_MODE=cloud
254
+
255
+ CMD [${command.map((c) => `"${c}"`).join(",")}]`;
256
+ return { content, image: imageScript, preRuns: docker.preRuns ?? [], postRuns: docker.postRuns ?? [], command };
257
+ }
258
+ static async from(app: App) {
259
+ const [configImp, baseDevEnv, libs, rootPackageJson] = await Promise.all([
260
+ import(`${app.cwdPath}/akan.config.ts`).then((mod) => mod.default),
261
+ WorkspaceExecutor.getBaseDevEnv(path.join(app.workspace.workspaceRoot, ".env")),
262
+ app.workspace.getLibs(),
263
+ app.workspace.getPackageJson(),
264
+ ]);
265
+ const config = typeof configImp === "function" ? configImp(app) : configImp;
266
+ return new AkanAppConfig(app, libs, rootPackageJson, config, baseDevEnv);
267
+ }
268
+ #resolveProductionDependencyVersion(lib: string) {
269
+ const rootVersion = this.rootPackageJson.dependencies?.[lib] ?? this.rootPackageJson.devDependencies?.[lib];
270
+ if (rootVersion) return rootVersion;
271
+ const akanPackageJson = getAkanPackageJson();
272
+ if (AKAN_RUNTIME_PACKAGES.has(lib))
273
+ return akanPackageJson.dependencies?.[lib] ?? akanPackageJson.peerDependencies?.[lib];
274
+ }
275
+ getProductionPackageJson(data: Partial<PackageJson> = {}): PackageJson {
276
+ return {
277
+ name: this.app.name,
278
+ description: this.app.name,
279
+ version: "1.0.0",
280
+ main: "./main.js",
281
+ dependencies: Object.fromEntries(
282
+ [...new Set([...this.externalLibs, ...SSR_RUNTIME_PACKAGES, ...NATIVE_RUNTIME_PACKAGES])].map((lib) => {
283
+ const version = this.#resolveProductionDependencyVersion(lib);
284
+ if (!version) throw new Error(`Dependency ${lib} not found in package.json`);
285
+ return [lib, version];
286
+ }),
287
+ ),
288
+ ...data,
289
+ };
290
+ }
291
+ }
292
+
293
+ let akanPackageJson: PackageJson | null = null;
294
+
295
+ function getAkanPackageJson() {
296
+ if (akanPackageJson) return akanPackageJson;
297
+ const sourceDir = path.dirname(fileURLToPath(import.meta.url));
298
+ const packageJsonPaths = [
299
+ path.join(sourceDir, "../../../akanjs/package.json"),
300
+ path.join(process.cwd(), "pkgs/akanjs/package.json"),
301
+ path.join(path.dirname(Bun.main), "node_modules/akanjs/package.json"),
302
+ ];
303
+ try {
304
+ packageJsonPaths.unshift(Bun.resolveSync("akanjs/package.json", sourceDir));
305
+ } catch {
306
+ // Monorepo source execution usually resolves Akan packages through tsconfig paths, not node_modules.
307
+ }
308
+ for (const packageJsonPath of packageJsonPaths) {
309
+ try {
310
+ akanPackageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as PackageJson;
311
+ return akanPackageJson;
312
+ } catch {
313
+ // Try the next known layout: source package first, bundled CLI package second.
314
+ }
315
+ }
316
+ akanPackageJson = { name: "akanjs", version: "0.0.0", description: "akanjs", dependencies: {} };
317
+ return akanPackageJson;
318
+ }
319
+
320
+ function mergeImageConfig(config: Partial<AkanImageConfig> = {}): AkanImageConfig {
321
+ return {
322
+ ...DEFAULT_AKAN_IMAGE_CONFIG,
323
+ ...config,
324
+ deviceSizes: config.deviceSizes ?? DEFAULT_AKAN_IMAGE_CONFIG.deviceSizes,
325
+ imageSizes: config.imageSizes ?? DEFAULT_AKAN_IMAGE_CONFIG.imageSizes,
326
+ formats: config.formats ?? DEFAULT_AKAN_IMAGE_CONFIG.formats,
327
+ qualities: config.qualities ?? DEFAULT_AKAN_IMAGE_CONFIG.qualities,
328
+ remotePatterns: config.remotePatterns ?? DEFAULT_AKAN_IMAGE_CONFIG.remotePatterns,
329
+ localPatterns: config.localPatterns ?? DEFAULT_AKAN_IMAGE_CONFIG.localPatterns,
330
+ };
331
+ }
332
+
333
+ export class AkanLibConfig implements LibConfigResult {
334
+ lib: Lib;
335
+ externalLibs: string[];
336
+ constructor(lib: Lib, config: DeepPartial<LibConfigResult>) {
337
+ this.lib = lib;
338
+ this.externalLibs = config?.externalLibs ?? [];
339
+ }
340
+ static async from(lib: Lib) {
341
+ const [configImp] = await Promise.all([import(`${lib.cwdPath}/akan.config.ts`).then((mod) => mod.default)]);
342
+ const config = typeof configImp === "function" ? configImp(lib) : configImp;
343
+ return new AkanLibConfig(lib, config);
344
+ }
345
+ }
346
+
347
+ // export const getCapacitorConfig = (configImp: AppConfig, appInfo: AppScanResult, tsconfig: TsConfigJson) => {
348
+ // const props: RunnerProps = {
349
+ // type: "app",
350
+ // name: appInfo.name,
351
+ // repoName: appInfo.repoName,
352
+ // serveDomain: appInfo.serveDomain,
353
+ // env: (process.env.AKAN_PUBLIC_ENV ?? "debug") as "testing" | "local" | "debug" | "develop" | "main",
354
+ // libs: appInfo.libDeps,
355
+ // tsconfig,
356
+ // };
357
+ // const config = typeof configImp === "function" ? configImp(props) : configImp;
358
+ // const akanConfig = makeAppConfig(config, props);
359
+ // return akanConfig;
360
+ // };
361
+
362
+ //! need to refactor
363
+ export const increaseBuildNum = async (app: App) => {
364
+ const appConfig = await AkanAppConfig.from(app);
365
+ const akanConfigPath = path.join(app.cwdPath, "akan.config.ts");
366
+ const akanConfig = fs.readFileSync(akanConfigPath, "utf8");
367
+ const akanConfigContent = akanConfig.replace(
368
+ `buildNum: ${appConfig.mobile.buildNum}`,
369
+ `buildNum: ${appConfig.mobile.buildNum + 1}`,
370
+ );
371
+ //? 개선할 여지가 있는지 확인
372
+ fs.writeFileSync(akanConfigPath, akanConfigContent);
373
+ };
374
+
375
+ export const decreaseBuildNum = async (app: App) => {
376
+ const appConfig = await AkanAppConfig.from(app);
377
+ const akanConfigPath = path.join(app.cwdPath, "akan.config.ts");
378
+ const akanConfig = fs.readFileSync(akanConfigPath, "utf8");
379
+ const akanConfigContent = akanConfig.replace(
380
+ `buildNum: ${appConfig.mobile.buildNum}`,
381
+ `buildNum: ${appConfig.mobile.buildNum - 1}`,
382
+ );
383
+ fs.writeFileSync(akanConfigPath, akanConfigContent);
384
+ };
@@ -0,0 +1,2 @@
1
+ export * from "./akanConfig";
2
+ export * from "./types";
@@ -0,0 +1,23 @@
1
+ export { archs } from "akanjs";
2
+ export type {
3
+ AkanConfigFile,
4
+ AkanMobileConfig,
5
+ AkanMobileTargetConfig,
6
+ AkanRouteConfig,
7
+ AppConfig,
8
+ AppConfigResult,
9
+ AppScanResult,
10
+ Arch,
11
+ DatabaseMode,
12
+ DeepPartial,
13
+ DockerConfig,
14
+ FileConventionScanResult,
15
+ LibConfig,
16
+ LibConfigResult,
17
+ LibScanResult,
18
+ MobileEnv,
19
+ MobilePermission,
20
+ PkgScanResult,
21
+ ScanResult,
22
+ WorkspaceScanResult,
23
+ } from "akanjs";