@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,69 @@
1
+ import { Logger } from "akanjs/common";
2
+
3
+ export interface ApplicationBuildPhaseResult {
4
+ id: string;
5
+ label: string;
6
+ durationMs: number;
7
+ summary?: string;
8
+ skipped?: boolean;
9
+ }
10
+
11
+ export interface ApplicationBuildResult {
12
+ phases: ApplicationBuildPhaseResult[];
13
+ durationMs: number;
14
+ outputDir: string;
15
+ artifactDir: string;
16
+ }
17
+
18
+ export interface ApplicationBuildProgressReporter {
19
+ phaseStart?(phase: Pick<ApplicationBuildPhaseResult, "id" | "label">): void;
20
+ phaseDone?(phase: ApplicationBuildPhaseResult): void;
21
+ phaseFail?(phase: Pick<ApplicationBuildPhaseResult, "id" | "label">, error: unknown): void;
22
+ }
23
+
24
+ export class ApplicationBuildReporter {
25
+ static create(): ApplicationBuildProgressReporter {
26
+ return {
27
+ phaseDone: (phase) => Logger.rawLog(ApplicationBuildReporter.formatPhaseLine(phase)),
28
+ };
29
+ }
30
+
31
+ static printSummary(result: ApplicationBuildResult) {
32
+ Logger.rawLog("");
33
+ Logger.rawLog(`Route artifacts: ${result.artifactDir}`);
34
+ Logger.rawLog(`Server output: ${result.outputDir}`);
35
+ Logger.rawLog(`Done in ${ApplicationBuildReporter.formatDuration(result.durationMs)}`);
36
+ }
37
+
38
+ static formatError(error: unknown): string {
39
+ if (error instanceof AggregateError) {
40
+ const nestedMessages = error.errors
41
+ .map(
42
+ (nestedError, index) =>
43
+ ApplicationBuildReporter.formatError(nestedError).trim() || `Unknown error ${index + 1}`,
44
+ )
45
+ .map((message) => message.replace(/^/gm, " "))
46
+ .join("\n");
47
+
48
+ return nestedMessages ? `${error.message}\n${nestedMessages}` : error.message;
49
+ }
50
+ if (error instanceof Error) {
51
+ const causeMessage = error.cause ? `\nCaused by: ${ApplicationBuildReporter.formatError(error.cause)}` : "";
52
+ return `${error.message}${causeMessage}`;
53
+ }
54
+ if (typeof error === "object" && error !== null && "message" in error) return String(error.message);
55
+ return String(error);
56
+ }
57
+
58
+ static formatDuration(ms: number): string {
59
+ if (ms < 1000) return `${ms}ms`;
60
+ if (ms < 60_000) return `${Math.round(ms / 100) / 10}s`;
61
+ const seconds = Math.floor(ms / 1000);
62
+ return `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
63
+ }
64
+
65
+ static formatPhaseLine(phase: ApplicationBuildPhaseResult): string {
66
+ const summary = phase.summary ? `: ${phase.summary}` : "";
67
+ return `✓ ${phase.label}${summary} (${ApplicationBuildReporter.formatDuration(phase.durationMs)})`;
68
+ }
69
+ }
@@ -0,0 +1,302 @@
1
+ import { mkdir, rm } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import type { BunPlugin } from "bun";
4
+ import type {
5
+ ApplicationBuildPhaseResult,
6
+ ApplicationBuildProgressReporter,
7
+ ApplicationBuildResult,
8
+ } from "./applicationBuildReporter";
9
+ import type { App } from "./commandDecorators";
10
+ import { AllRoutesBuilder, CsrArtifactBuilder, precompressArtifacts, SsrBaseArtifactBuilder } from "./frontendBuild";
11
+ import { Spinner } from "./spinner";
12
+
13
+ export interface TypecheckOptions {
14
+ clean?: boolean;
15
+ incremental?: boolean;
16
+ }
17
+
18
+ export type BuildPhaseId = "prepare" | "typecheck" | "backend" | "ssr" | "csr" | "compress" | "metadata";
19
+
20
+ export type BuildPhaseResult = ApplicationBuildPhaseResult & { id: BuildPhaseId };
21
+ export type BuildResult = ApplicationBuildResult;
22
+ export type BuildProgressReporter = ApplicationBuildProgressReporter;
23
+ export interface ApplicationBuildRunnerOptions {
24
+ fast?: boolean;
25
+ reporter?: BuildProgressReporter;
26
+ }
27
+ export interface BuildOptions {
28
+ spinner?: boolean;
29
+ }
30
+ export interface BuildPhaseRunOptions {
31
+ spinner?: boolean;
32
+ }
33
+
34
+ const BUILD_PHASE_EMOJIS: Record<BuildPhaseId, string> = {
35
+ prepare: "🧹",
36
+ typecheck: "🔎",
37
+ backend: "📦",
38
+ ssr: "🧭",
39
+ csr: "🎨",
40
+ compress: "🗜️",
41
+ metadata: "📝",
42
+ };
43
+
44
+ const SSR_RENDER_EXTERNALS = [
45
+ "react",
46
+ "react/jsx-runtime",
47
+ "react/jsx-dev-runtime",
48
+ "react-dom",
49
+ "react-dom/server.browser",
50
+ "react-server-dom-webpack/client.node",
51
+ "react-server-dom-webpack/client.browser",
52
+ ] as const;
53
+
54
+ const TYPECHECK_WORKER_CODE = `
55
+ import { TypeChecker } from "@akanjs/devkit";
56
+
57
+ try {
58
+ const configPath = process.env.AKAN_TYPECHECK_TSCONFIG;
59
+ if (!configPath) throw new Error("AKAN_TYPECHECK_TSCONFIG is required");
60
+ const result = TypeChecker.checkProject(configPath);
61
+ if (result.errors.length > 0) {
62
+ console.error(result.message);
63
+ process.exit(1);
64
+ }
65
+ } catch (error) {
66
+ console.error(error instanceof Error ? error.message : String(error));
67
+ process.exit(1);
68
+ }
69
+ `;
70
+
71
+ export class ApplicationBuildRunner {
72
+ #app: App;
73
+ #fast: boolean;
74
+ #reporter?: BuildProgressReporter;
75
+ #startedAt = Date.now();
76
+ #phases: BuildPhaseResult[] = [];
77
+
78
+ constructor(app: App, { fast = false, reporter }: ApplicationBuildRunnerOptions = {}) {
79
+ this.#app = app;
80
+ this.#fast = fast;
81
+ this.#reporter = reporter;
82
+ }
83
+
84
+ async build({ spinner = false }: BuildOptions = {}): Promise<BuildResult> {
85
+ // serial build is needed because of Bun.build is unstable for parallel build
86
+ const phaseOptions = { spinner };
87
+ await this.#runPhase("prepare", "Preparing output directory", () => this.#app.prepareCommand("build"), undefined, {
88
+ spinner,
89
+ });
90
+ if (!this.#fast) await this.#runPhase("typecheck", "Typechecking", () => this.typecheck(), undefined, phaseOptions);
91
+ await this.#runPhase(
92
+ "backend",
93
+ "Compiling backend",
94
+ () => this.#buildBackend(),
95
+ (result) => `${result.entrypoints} entrypoints, ${result.outputs} outputs`,
96
+ phaseOptions,
97
+ );
98
+ await this.#runPhase(
99
+ "ssr",
100
+ "Building SSR route artifacts",
101
+ () => this.#buildSsr(),
102
+ (result) =>
103
+ result
104
+ ? `${result.allRoutes.manifest.routeIds.length} routes, ${result.allRoutes.manifest.knownEntries.length} entries`
105
+ : "skipped",
106
+ phaseOptions,
107
+ );
108
+ await this.#runPhase(
109
+ "csr",
110
+ "Building CSR assets",
111
+ () => this.#buildCsr(),
112
+ (result) => result?.outputDir ?? "skipped",
113
+ phaseOptions,
114
+ );
115
+ await this.#runPhase(
116
+ "compress",
117
+ "Compressing static assets",
118
+ () => precompressArtifacts(this.#app),
119
+ (result) =>
120
+ result.files > 0
121
+ ? `${result.files} files, ${ApplicationBuildRunner.formatBytes(result.inputBytes)} -> ${ApplicationBuildRunner.formatBytes(result.outputBytes)}`
122
+ : "no files",
123
+ phaseOptions,
124
+ );
125
+ await this.#runPhase("metadata", "Writing production metadata", () => this.#buildAppMeta(), undefined, {
126
+ spinner,
127
+ });
128
+ return {
129
+ phases: this.#phases,
130
+ durationMs: Date.now() - this.#startedAt,
131
+ outputDir: this.#app.dist.cwdPath,
132
+ artifactDir: path.join(this.#app.dist.cwdPath, ".akan/artifact"),
133
+ };
134
+ }
135
+
136
+ async typecheck(options: TypecheckOptions = {}) {
137
+ const { clean = false, incremental = true } = options;
138
+ await this.#app.getPageKeys({ refresh: true });
139
+ const { typecheckDir, tsconfigPath } = await this.#writeTypecheckTsconfig({ incremental });
140
+ if (clean) await rm(path.join(typecheckDir, "tsconfig.tsbuildinfo"), { force: true });
141
+ await this.#checkProjectInChildProcess(tsconfigPath);
142
+ }
143
+
144
+ async #runPhase<T>(
145
+ id: BuildPhaseId,
146
+ label: string,
147
+ task: () => Promise<T>,
148
+ summarize?: (result: T) => string | undefined,
149
+ options: BuildPhaseRunOptions = {},
150
+ ) {
151
+ this.#reporter?.phaseStart?.({ id, label });
152
+ const phaseStartedAt = Date.now();
153
+ const spinner = options.spinner
154
+ ? new Spinner(label, { prefix: `${BUILD_PHASE_EMOJIS[id]} ${id}` }).start()
155
+ : undefined;
156
+ try {
157
+ const result = await task();
158
+ const phase = { id, label, durationMs: Date.now() - phaseStartedAt, summary: summarize?.(result) };
159
+ this.#phases.push(phase);
160
+ const summary = phase.summary ? `: ${phase.summary}` : "";
161
+ spinner?.succeed(`${label}${summary}`);
162
+ this.#reporter?.phaseDone?.(phase);
163
+ return result;
164
+ } catch (error) {
165
+ spinner?.fail(`${label} failed`);
166
+ this.#reporter?.phaseFail?.({ id, label }, error);
167
+ throw error;
168
+ }
169
+ }
170
+
171
+ async #buildAppMeta() {
172
+ const akanConfig = await this.#app.getConfig();
173
+ await Promise.all([
174
+ this.#app.dist.writeJson("package.json", akanConfig.getProductionPackageJson()),
175
+ this.#app.dist.writeFile(`${this.#app.dist.cwdPath}/Dockerfile`, akanConfig.docker.content),
176
+ ]);
177
+ }
178
+
179
+ async #buildBackend() {
180
+ const akanConfig = await this.#app.getConfig();
181
+ const backendExternals = [...new Set([...akanConfig.externalLibs, ...SSR_RENDER_EXTERNALS])];
182
+ const backendEntryPoints = [`${this.#app.cwdPath}/main.ts`, `${this.#app.cwdPath}/server.ts`];
183
+ for (const entrypoint of backendEntryPoints) {
184
+ if (!(await Bun.file(entrypoint).exists())) throw new Error(`Backend entrypoint not found: ${entrypoint}`);
185
+ }
186
+ const backendResult = await this.#buildOrThrow("backend", {
187
+ entrypoints: backendEntryPoints,
188
+ outdir: this.#app.dist.cwdPath,
189
+ target: "bun",
190
+ minify: true,
191
+ define: { "process.env.NODE_ENV": JSON.stringify("production") },
192
+ plugins: backendExternals.length > 0 ? [this.#createExternalSpecifiersPlugin(backendExternals)] : [],
193
+ });
194
+ const rscWorkerResult = await this.#buildOrThrow("rsc-worker", {
195
+ entrypoints: [this.#resolveRscWorkerBuildEntry()],
196
+ outdir: this.#app.dist.cwdPath,
197
+ target: "bun",
198
+ minify: true,
199
+ naming: { entry: "[name].[ext]", chunk: "chunk-[hash].[ext]" },
200
+ conditions: ["react-server"],
201
+ // `akan build` must embed production react-server-dom regardless of the shell's NODE_ENV.
202
+ define: { "process.env.NODE_ENV": JSON.stringify("production") },
203
+ plugins:
204
+ akanConfig.externalLibs.length > 0 ? [this.#createExternalSpecifiersPlugin(akanConfig.externalLibs)] : [],
205
+ });
206
+ return {
207
+ entrypoints: backendEntryPoints.length + 1,
208
+ outputs: backendResult.outputs.length + rscWorkerResult.outputs.length,
209
+ };
210
+ }
211
+
212
+ #resolveRscWorkerBuildEntry(): string {
213
+ try {
214
+ return Bun.resolveSync("akanjs/server/rsc-worker", import.meta.dir);
215
+ } catch {
216
+ return path.join(this.#app.workspace.workspaceRoot, "pkgs/akanjs/server/rscWorker.tsx");
217
+ }
218
+ }
219
+
220
+ async #buildCsr() {
221
+ return await new CsrArtifactBuilder(this.#app, "build").build();
222
+ }
223
+
224
+ async #buildSsr() {
225
+ const pageKeys = await this.#app.getPageKeys();
226
+ if (pageKeys.length === 0) {
227
+ this.#app.log(`[cli] no route files under ${this.#app.cwdPath}/page — skipping SSR build`);
228
+ return null;
229
+ }
230
+ const base = await new SsrBaseArtifactBuilder(this.#app, "build").build();
231
+ const allRoutes = await new AllRoutesBuilder(this.#app, base.artifact, "build").build();
232
+ return { base, allRoutes };
233
+ }
234
+
235
+ async #writeTypecheckTsconfig({ incremental = true }: TypecheckOptions = {}) {
236
+ const typecheckDir = path.join(this.#app.cwdPath, ".akan", "typecheck");
237
+ await mkdir(typecheckDir, { recursive: true });
238
+ const tsconfig = {
239
+ extends: "../../tsconfig.json",
240
+ compilerOptions: {
241
+ noEmit: true,
242
+ incremental,
243
+ tsBuildInfoFile: "./tsconfig.tsbuildinfo",
244
+ },
245
+ include: [
246
+ "../../main.ts",
247
+ "../../server.ts",
248
+ "../../client.ts",
249
+ "../../page/**/*.ts",
250
+ "../../page/**/*.tsx",
251
+ "../../../../pkgs/akanjs/*/types/**/*.d.ts",
252
+ ],
253
+ references: [],
254
+ };
255
+ const tsconfigPath = path.join(typecheckDir, "tsconfig.json");
256
+ await Bun.write(tsconfigPath, `${JSON.stringify(tsconfig, null, 2)}\n`);
257
+ return { typecheckDir, tsconfigPath };
258
+ }
259
+
260
+ async #checkProjectInChildProcess(tsconfigPath: string) {
261
+ const proc = Bun.spawn([process.execPath, "--eval", TYPECHECK_WORKER_CODE], {
262
+ cwd: this.#app.workspace.workspaceRoot,
263
+ env: this.#app.getCommandEnv({
264
+ AKAN_COMMAND_TYPE: "typecheck",
265
+ AKAN_TYPECHECK_TSCONFIG: tsconfigPath,
266
+ }),
267
+ stdout: "pipe",
268
+ stderr: "pipe",
269
+ });
270
+ const [stdout, stderr, exitCode] = await Promise.all([
271
+ new Response(proc.stdout).text(),
272
+ new Response(proc.stderr).text(),
273
+ proc.exited,
274
+ ]);
275
+ if (exitCode !== 0) throw new Error((stderr || stdout).trim() || `Typecheck failed with exit code ${exitCode}`);
276
+ }
277
+
278
+ async #buildOrThrow(label: string, config: Bun.BuildConfig): Promise<Bun.BuildOutput> {
279
+ const result = await Bun.build(config);
280
+ if (!result.success) throw new AggregateError(result.logs, `[${label}] Bun.build failed`);
281
+ return result;
282
+ }
283
+
284
+ #createExternalSpecifiersPlugin(specifiers: readonly string[]): BunPlugin {
285
+ const uniqueSpecifiers = [...new Set(specifiers)];
286
+ const escaped = uniqueSpecifiers.map((specifier) => specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
287
+ const filter = new RegExp(`^(${escaped.join("|")})(?:/.*)?$`);
288
+
289
+ return {
290
+ name: "akan-backend-externalize-specifiers",
291
+ setup(build) {
292
+ build.onResolve({ filter }, (args) => ({ path: args.path, external: true }));
293
+ },
294
+ };
295
+ }
296
+
297
+ static formatBytes(bytes: number): string {
298
+ if (bytes < 1024) return `${bytes}B`;
299
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
300
+ return `${(bytes / 1024 / 1024).toFixed(1)}MB`;
301
+ }
302
+ }
@@ -0,0 +1,206 @@
1
+ import { cp, mkdir, rm } from "node:fs/promises";
2
+ import type { App } from "./commandDecorators";
3
+ import { FileSys } from "./fileSys";
4
+ import { uploadRelease } from "./uploadRelease";
5
+
6
+ export interface ReleaseSourceOptions {
7
+ rebuild?: boolean;
8
+ buildNum?: number;
9
+ environment?: string;
10
+ local?: boolean;
11
+ }
12
+
13
+ interface ApplicationReleasePackagerOptions {
14
+ build: () => Promise<void>;
15
+ }
16
+
17
+ export class ApplicationReleasePackager {
18
+ #app: App;
19
+ #build: () => Promise<void>;
20
+
21
+ constructor(app: App, options: ApplicationReleasePackagerOptions) {
22
+ this.#app = app;
23
+ this.#build = options.build;
24
+ }
25
+
26
+ async releaseSource({
27
+ rebuild,
28
+ buildNum = 0,
29
+ environment = "debug",
30
+ local = true,
31
+ }: ReleaseSourceOptions = {}): Promise<void> {
32
+ const { platformVersion } = await this.#prepareReleaseBuild({ rebuild, buildNum });
33
+ await this.#writeSourceArchive({ readme: this.#confidentialReadme() });
34
+ await uploadRelease(this.#app.name, {
35
+ local,
36
+ buildNum,
37
+ environment,
38
+ platformVersion,
39
+ workspaceRoot: this.#app.workspace.cwdPath,
40
+ });
41
+ }
42
+
43
+ async compressProjectFiles({ rebuild, buildNum = 0 }: ReleaseSourceOptions = {}): Promise<void> {
44
+ await this.#prepareReleaseBuild({ rebuild, buildNum });
45
+ await this.#writeSourceArchive({ readme: this.#publicReadme() });
46
+ }
47
+
48
+ async #prepareReleaseBuild({ rebuild, buildNum }: { rebuild?: boolean; buildNum: number }) {
49
+ const akanConfig = await this.#app.getConfig();
50
+ const platformVersion = akanConfig.mobile.version;
51
+ const buildRoot = `${this.#app.workspace.workspaceRoot}/releases/builds/${this.#app.name}`;
52
+ if (await FileSys.dirExists(buildRoot)) await rm(buildRoot, { recursive: true, force: true });
53
+ await mkdir(buildRoot, { recursive: true });
54
+ if (rebuild || !(await FileSys.dirExists(`${this.#app.dist.cwdPath}/backend`))) await this.#build();
55
+
56
+ const buildVersion = `${platformVersion}-${buildNum}`;
57
+ const buildPath = `${buildRoot}/${buildVersion}`;
58
+ await mkdir(buildPath, { recursive: true });
59
+ await cp(`${this.#app.dist.cwdPath}/backend`, `${buildPath}/backend`, { recursive: true });
60
+
61
+ await cp(this.#app.dist.cwdPath, buildRoot, { recursive: true });
62
+ await rm(`${buildRoot}/frontend/.next`, { recursive: true, force: true });
63
+
64
+ await this.#app.workspace.spawn("tar", [
65
+ "-zcf",
66
+ `${this.#app.workspace.workspaceRoot}/releases/builds/${this.#app.name}-release.tar.gz`,
67
+ "-C",
68
+ buildRoot,
69
+ "./",
70
+ ]);
71
+ await this.#writeCsrZipIfPresent();
72
+ return { platformVersion };
73
+ }
74
+
75
+ async #writeCsrZipIfPresent(): Promise<void> {
76
+ if (!(await FileSys.dirExists(`${this.#app.dist.cwdPath}/csr`))) return;
77
+ await cp(`${this.#app.dist.cwdPath}/csr`, "./csr", { recursive: true });
78
+ await this.#app.workspace.spawn("zip", [
79
+ "-r",
80
+ `${this.#app.workspace.workspaceRoot}/releases/builds/${this.#app.name}-appBuild.zip`,
81
+ "./csr",
82
+ ]);
83
+ await rm("./csr", { recursive: true, force: true });
84
+ }
85
+
86
+ async #writeSourceArchive({ readme }: { readme: string }): Promise<void> {
87
+ const sourceRoot = `${this.#app.workspace.workspaceRoot}/releases/sources/${this.#app.name}`;
88
+ await this.#resetSourceRoot(sourceRoot);
89
+ await cp(this.#app.dist.cwdPath, `${sourceRoot}/apps/${this.#app.name}`, { recursive: true });
90
+
91
+ const libDeps = ["social", "shared", "platform", "util"];
92
+ await Promise.all(
93
+ libDeps.map((lib) =>
94
+ cp(`${this.#app.workspace.cwdPath}/libs/${lib}`, `${sourceRoot}/libs/${lib}`, { recursive: true }),
95
+ ),
96
+ );
97
+ await Promise.all(
98
+ [".next", "ios", "android", "public/libs"].map(async (path) => {
99
+ const targetPath = `${sourceRoot}/apps/${this.#app.name}/${path}`;
100
+ if (await FileSys.dirExists(targetPath)) await rm(targetPath, { recursive: true, force: true });
101
+ }),
102
+ );
103
+
104
+ const syncPaths = [".husky", ".gitignore", "package.json"];
105
+ await Promise.all(
106
+ syncPaths.map((path) =>
107
+ cp(`${this.#app.workspace.cwdPath}/${path}`, `${sourceRoot}/${path}`, { recursive: true }),
108
+ ),
109
+ );
110
+ await this.#writeSourceTsconfig(sourceRoot, libDeps);
111
+ await Bun.write(`${sourceRoot}/README.md`, readme);
112
+ await this.#app.workspace.spawn("tar", [
113
+ "-zcf",
114
+ `${this.#app.workspace.cwdPath}/releases/sources/${this.#app.name}-source.tar.gz`,
115
+ "-C",
116
+ sourceRoot,
117
+ "./",
118
+ ]);
119
+ }
120
+
121
+ async #resetSourceRoot(sourceRoot: string): Promise<void> {
122
+ if (await FileSys.dirExists(sourceRoot)) {
123
+ const maxRetry = 3;
124
+ for (let i = 0; i < maxRetry; i++) {
125
+ try {
126
+ await rm(sourceRoot, { recursive: true, force: true });
127
+ } catch {
128
+ //
129
+ }
130
+ }
131
+ }
132
+ await mkdir(sourceRoot, { recursive: true });
133
+ }
134
+
135
+ async #writeSourceTsconfig(sourceRoot: string, libDeps: string[]): Promise<void> {
136
+ const tsconfig = (await this.#app.workspace.readJson("tsconfig.json")) as {
137
+ compilerOptions: { paths: Record<string, string[]> };
138
+ };
139
+ const pathEntries: [string, string[]][] = [[`@${this.#app.name}/*`, [`apps/${this.#app.name}/*`]]];
140
+ for (const lib of libDeps) {
141
+ pathEntries.push([`@${lib}`, [`libs/${lib}/index.ts`]], [`@${lib}/*`, [`libs/${lib}/*`]]);
142
+ }
143
+ tsconfig.compilerOptions.paths = Object.fromEntries(pathEntries) as Record<string, string[]>;
144
+ await Bun.write(`${sourceRoot}/tsconfig.json`, JSON.stringify(tsconfig, null, 2));
145
+ }
146
+
147
+ #confidentialReadme(): string {
148
+ return `# ${this.#app.name}
149
+ 본 프로젝트의 소스코드 및 관련자료는 모두 비밀정보로 관리됩니다.
150
+
151
+ ## Get Started
152
+ Run the code below.
153
+ \`\`\`
154
+ bun install -g bun
155
+ bun i
156
+
157
+ cat <<EOF >> .env
158
+ # ENV For Server => debug | debug.local | develop | develop.local | main | main.local
159
+ SERVER_ENV=debug.local
160
+ # Run Mode For Server => federation | batch | all
161
+ SERVER_MODE=federation
162
+ # ENV For Client => debug | debug.local | develop | develop.local | main | main.local
163
+ AKAN_PUBLIC_CLIENT_ENV=debug.local
164
+ ANALYZE=false
165
+ EOF
166
+
167
+ akn start-backend ${this.#app.name}
168
+ # or akn start-frontend ${this.#app.name}, etc
169
+ \`\`\`
170
+
171
+ ## Build
172
+ Run the code below.
173
+ \`\`\`
174
+ akn build-backend ${this.#app.name}
175
+ # or akn build-frontend ${this.#app.name}, etc
176
+ \`\`\`
177
+ `;
178
+ }
179
+
180
+ #publicReadme(): string {
181
+ return `# ${this.#app.name}
182
+ ## Get Started
183
+ Run the code below.
184
+ \`\`\`
185
+ cat <<EOF >> .env
186
+ # ENV For Server => debug | debug.local | develop | develop.local | main | main.local
187
+ SERVER_ENV=debug.local
188
+ # Run Mode For Server => federation | batch | all
189
+ SERVER_MODE=federation
190
+ # ENV For Client => debug | debug.local | develop | develop.local | main | main.local
191
+ AKAN_PUBLIC_CLIENT_ENV=debug.local
192
+ ANALYZE=false
193
+ EOF
194
+
195
+ akan start ${this.#app.name}
196
+ \`\`\`
197
+
198
+ ## Build
199
+ Run the code below.
200
+ \`\`\`
201
+ akan build ${this.#app.name}
202
+ # or akn build-frontend ${this.#app.name}, etc
203
+ \`\`\`
204
+ `;
205
+ }
206
+ }