@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,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
+ }
@@ -0,0 +1,155 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import type { App } from "../commandDecorators";
4
+
5
+ export interface PageEntry {
6
+ key: string;
7
+ moduleAbsPath: string;
8
+ seedAbsPaths?: string[];
9
+ }
10
+
11
+ const LAYOUT_KEY_RE = /^\.\/(.+\/)?_layout\.(tsx|ts|jsx|js)$/;
12
+
13
+ async function appHasStModule(appCwdPath: string): Promise<boolean> {
14
+ return Bun.file(path.join(appCwdPath, "lib", "st.ts")).exists();
15
+ }
16
+
17
+ const IMPLICIT_LAYOUT_DIR = path.join(".akan", "generated", "root-layouts");
18
+
19
+ interface RootBoundary {
20
+ sourceKey: string | null;
21
+ sourceAbsPath: string | null;
22
+ segments: string[];
23
+ }
24
+
25
+ function getRootBoundarySegments(key: string): string[] | null {
26
+ const match = LAYOUT_KEY_RE.exec(key);
27
+ if (!match) return null;
28
+ const prefix = match[1]?.replace(/\/$/, "");
29
+ if (!prefix) return [];
30
+ return prefix.split("/").filter(Boolean);
31
+ }
32
+
33
+ function implicitRootLayoutKey(segments: string[]): string {
34
+ return `./${[...segments, "__root_layout"].join("/")}.tsx`;
35
+ }
36
+
37
+ function implicitRootLayoutAbsPath(appCwdPath: string, segments: string[]): string {
38
+ const filename = segments.length ? `${segments.join("__")}__root_layout.tsx` : "__root_layout.tsx";
39
+ return path.join(path.resolve(appCwdPath), IMPLICIT_LAYOUT_DIR, filename);
40
+ }
41
+
42
+ function isRootBoundarySegments(segments: string[], basePaths: Iterable<string>): boolean {
43
+ const firstVisibleIndex = segments.findIndex((segment) => !/^\(.+\)$/.test(segment));
44
+ if (firstVisibleIndex === -1) return segments.length <= 1;
45
+ if (segments.slice(firstVisibleIndex + 1).some((segment) => /^\(.+\)$/.test(segment))) return false;
46
+ const visible = segments.slice(firstVisibleIndex);
47
+ const allowedBasePaths = new Set([...basePaths].map((basePath) => basePath.trim()).filter(Boolean));
48
+ return visible.length === 1 && (firstVisibleIndex > 0 || allowedBasePaths.has(visible[0] ?? ""));
49
+ }
50
+
51
+ function findRootBoundaries(pageKeys: string[], appCwdPath: string, basePaths: Iterable<string>): RootBoundary[] {
52
+ const boundaries = new Map<string, RootBoundary>();
53
+ for (const key of pageKeys) {
54
+ const segments = getRootBoundarySegments(key);
55
+ if (!segments) continue;
56
+ if (!isRootBoundarySegments(segments, basePaths)) continue;
57
+ const id = segments.join("/");
58
+ boundaries.set(id, {
59
+ sourceKey: key,
60
+ sourceAbsPath: path.resolve(appCwdPath, "page", key.replace(/^\.\//, "")),
61
+ segments,
62
+ });
63
+ }
64
+ const hasExplicitRootBoundary = [...boundaries.values()].some((boundary) => boundary.segments.length === 0);
65
+ if (!hasExplicitRootBoundary && boundaries.size === 0) {
66
+ boundaries.set("", { sourceKey: null, sourceAbsPath: null, segments: [] });
67
+ }
68
+ return [...boundaries.values()].sort((a, b) => a.segments.join("/").localeCompare(b.segments.join("/")));
69
+ }
70
+
71
+ function routePrefixForSegments(segments: string[]): string | null {
72
+ const visible = segments.filter((segment) => !/^\(.+\)$/.test(segment));
73
+ return visible[0] ?? null;
74
+ }
75
+
76
+ async function assertEnvClientConvention(appCwdPath: string, appName: string) {
77
+ const envPath = path.join(appCwdPath, "env", "env.client.ts");
78
+ if (!(await Bun.file(envPath).exists())) {
79
+ throw new Error(
80
+ `[route-convention] app "${appName}" must provide env/env.client.ts exporting "env" for generated System.Provider`,
81
+ );
82
+ }
83
+ }
84
+
85
+ async function writeGeneratedRootLayoutFile(opts: {
86
+ appCwdPath: string;
87
+ appName: string;
88
+ boundary: RootBoundary;
89
+ includeStInit: boolean;
90
+ }): Promise<string> {
91
+ await assertEnvClientConvention(opts.appCwdPath, opts.appName);
92
+ const absPath = implicitRootLayoutAbsPath(opts.appCwdPath, opts.boundary.segments);
93
+ await mkdir(path.dirname(absPath), { recursive: true });
94
+ const sourceRel = opts.boundary.sourceAbsPath
95
+ ? path.relative(path.dirname(absPath), opts.boundary.sourceAbsPath).split(path.sep).join("/")
96
+ : null;
97
+ const sourceSpecifier = sourceRel ? (sourceRel.startsWith(".") ? sourceRel : `./${sourceRel}`) : null;
98
+ const clientImport = opts.includeStInit
99
+ ? `import { st } from "@apps/${opts.appName}/client";\nvoid st;\n`
100
+ : `import "@apps/${opts.appName}/client";\n`;
101
+ const prefix = routePrefixForSegments(opts.boundary.segments);
102
+ const userImport = sourceSpecifier
103
+ ? `import UserLayout, * as userLayout from ${JSON.stringify(sourceSpecifier)};\n`
104
+ : "const UserLayout = ({ children }) => children;\nconst userLayout = {};\n";
105
+ const source = `import type { LayoutProps, PageProps } from "akanjs/client";\nimport { loadFonts } from "akanjs/client";\nimport { System } from "akanjs/ui";\nimport { env } from "@apps/${opts.appName}/env/env.client";\n${clientImport}${userImport}\nconst userFonts = userLayout.fonts ?? [];\nconst defaultFonts = userFonts.filter((font) => font.default);\nif (defaultFonts.length > 1) throw new Error("[route-convention] only one default font is allowed per root layout");\nconst defaultFont = defaultFonts[0];\nconst defaultFontClassName = defaultFont ? (defaultFont.className ?? \`font-\${defaultFont.name}\`) : undefined;\n\nexport async function generateHead(props: PageProps) {\n if (userLayout.generateHead) return userLayout.generateHead(props);\n return userLayout.head;\n}\n\nexport default function GeneratedLayout({ children, params, searchParams }: LayoutProps) {\n return (\n <System.Provider\n of={GeneratedLayout as never}\n appName=${JSON.stringify(opts.appName)}\n ${prefix ? `prefix=${JSON.stringify(prefix)}\n ` : ""}params={params}\n manifest={userLayout.manifest}\n env={env}\n theme={userLayout.theme}\n fonts={loadFonts(userFonts)}\n className={defaultFontClassName}\n gaTrackingId={userLayout.gaTrackingId}\n layoutStyle={userLayout.layoutStyle}\n reconnect={userLayout.reconnect ?? false}\n >\n <UserLayout params={params} searchParams={searchParams}>{children}</UserLayout>\n </System.Provider>\n );\n}\n`;
106
+ await Bun.write(absPath, source);
107
+ return absPath;
108
+ }
109
+
110
+ /**
111
+ * When no root `page/_layout.*` exists on disk, merge a generated implicit root layout
112
+ * (with generated client runtime registration and optional `void st` when `lib/st.ts` exists).
113
+ */
114
+ export async function resolveSsrPageEntries(opts: {
115
+ appCwdPath: string;
116
+ appName: string;
117
+ pageKeys: string[];
118
+ basePaths?: Iterable<string>;
119
+ }): Promise<PageEntry[]> {
120
+ const absPageDir = path.resolve(opts.appCwdPath, "page");
121
+ const hasSt = await appHasStModule(opts.appCwdPath);
122
+ const basePaths = opts.basePaths ?? [];
123
+ const rootLayoutKeys = new Set(
124
+ opts.pageKeys.filter((key) => {
125
+ const segments = getRootBoundarySegments(key);
126
+ return segments !== null && isRootBoundarySegments(segments, basePaths);
127
+ }),
128
+ );
129
+ const base = opts.pageKeys
130
+ .filter((key) => !rootLayoutKeys.has(key))
131
+ .map((key) => ({
132
+ key,
133
+ moduleAbsPath: path.resolve(absPageDir, key),
134
+ }));
135
+ const generated = await Promise.all(
136
+ findRootBoundaries(opts.pageKeys, opts.appCwdPath, basePaths).map(async (boundary) => ({
137
+ key: implicitRootLayoutKey(boundary.segments),
138
+ moduleAbsPath: await writeGeneratedRootLayoutFile({
139
+ appCwdPath: opts.appCwdPath,
140
+ appName: opts.appName,
141
+ boundary,
142
+ includeStInit: hasSt && boundary.segments.length === 0,
143
+ }),
144
+ seedAbsPaths: boundary.sourceAbsPath ? [boundary.sourceAbsPath] : [],
145
+ })),
146
+ );
147
+ const entries = [...base, ...generated];
148
+ entries.sort((a, b) => a.key.localeCompare(b.key));
149
+ return entries;
150
+ }
151
+
152
+ export async function resolveSsrPageEntriesForApp(app: App, pageKeys: string[]): Promise<PageEntry[]> {
153
+ const config = await app.getConfig();
154
+ return resolveSsrPageEntries({ appCwdPath: app.cwdPath, appName: app.name, pageKeys, basePaths: config.basePaths });
155
+ }
@@ -0,0 +1 @@
1
+ export * from "./routeSeedIndex";