@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
package/executors.ts ADDED
@@ -0,0 +1,1390 @@
1
+ import {
2
+ type ChildProcess,
3
+ type ExecOptions,
4
+ exec,
5
+ type ForkOptions,
6
+ fork,
7
+ type SpawnOptions,
8
+ spawn,
9
+ } from "node:child_process";
10
+ import { readFileSync } from "node:fs";
11
+ import { copyFile, mkdir, readdir as readDirEntries, stat } from "node:fs/promises";
12
+ import path from "node:path";
13
+ import {
14
+ capitalize,
15
+ isRouteSourceFile,
16
+ Logger,
17
+ parseRouteModuleKey,
18
+ validatePageSourceFile,
19
+ validateSubRoutePageKey,
20
+ } from "akanjs/common";
21
+ import { $ } from "bun";
22
+ import chalk from "chalk";
23
+ import ts from "typescript";
24
+ import { AkanAppConfig, AkanLibConfig, decreaseBuildNum, increaseBuildNum } from "./akanConfig";
25
+ import { FileSys } from "./fileSys";
26
+ import { getDirname } from "./getDirname";
27
+ import { Linter } from "./linter";
28
+ import { AppInfo, LibInfo, PkgInfo, WorkspaceInfo } from "./scanInfo";
29
+ import { Spinner } from "./spinner";
30
+ import { TypeChecker } from "./typeChecker";
31
+ import type { FileContent, PackageJson, TsConfigJson } from "./types";
32
+
33
+ const staticTemplateFileExtensions = new Set([
34
+ ".avif",
35
+ ".bmp",
36
+ ".cjs",
37
+ ".css",
38
+ ".eot",
39
+ ".gif",
40
+ ".html",
41
+ ".ico",
42
+ ".jpeg",
43
+ ".jpg",
44
+ ".js",
45
+ ".json",
46
+ ".map",
47
+ ".md",
48
+ ".mjs",
49
+ ".mp3",
50
+ ".mp4",
51
+ ".ogg",
52
+ ".otf",
53
+ ".pdf",
54
+ ".png",
55
+ ".svg",
56
+ ".ttf",
57
+ ".txt",
58
+ ".wasm",
59
+ ".wav",
60
+ ".webm",
61
+ ".webp",
62
+ ".woff",
63
+ ".woff2",
64
+ ".xml",
65
+ ]);
66
+
67
+ export const execEmoji = {
68
+ workspace: "🏠",
69
+ app: "🚀",
70
+ lib: "🔧",
71
+ pkg: "📦",
72
+ dist: "💿",
73
+ module: "⚙️",
74
+ default: "✈️", // for sys executor
75
+ };
76
+
77
+ const parseEnvFile = (envPath: string): Record<string, string> => {
78
+ const env: Record<string, string> = {};
79
+ const content = (() => {
80
+ try {
81
+ return readFileSync(envPath, "utf8");
82
+ } catch {
83
+ return "";
84
+ }
85
+ })();
86
+ for (const line of content.split(/\r?\n/)) {
87
+ const trimmed = line.trim();
88
+ if (!trimmed || trimmed.startsWith("#")) continue;
89
+ const normalized = trimmed.startsWith("export ") ? trimmed.slice("export ".length).trim() : trimmed;
90
+ const separatorIndex = normalized.indexOf("=");
91
+ if (separatorIndex <= 0) continue;
92
+ const key = normalized.slice(0, separatorIndex).trim();
93
+ let value = normalized.slice(separatorIndex + 1).trim();
94
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
95
+ value = value.slice(1, -1);
96
+ }
97
+ env[key] = value;
98
+ }
99
+ return env;
100
+ };
101
+
102
+ const PAGE_ROUTE_EXPORTS = new Set(["default", "pageConfig", "head", "generateHead", "Loading"]);
103
+ const ROOT_LAYOUT_EXPORTS = new Set([
104
+ "default",
105
+ "head",
106
+ "generateHead",
107
+ "fonts",
108
+ "manifest",
109
+ "theme",
110
+ "reconnect",
111
+ "layoutStyle",
112
+ "gaTrackingId",
113
+ "Loading",
114
+ ]);
115
+ const LAYOUT_ROUTE_EXPORTS = new Set(["default", "head", "generateHead", "Loading"]);
116
+
117
+ function validateRouteSourceExports(
118
+ source: string,
119
+ filePath: string,
120
+ kind: "page" | "layout",
121
+ options: { rootLayout?: boolean } = {},
122
+ ) {
123
+ const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
124
+ const allowed =
125
+ kind === "page" ? PAGE_ROUTE_EXPORTS : options.rootLayout ? ROOT_LAYOUT_EXPORTS : LAYOUT_ROUTE_EXPORTS;
126
+ const exported = new Set<string>();
127
+ const assertExport = (name: string) => {
128
+ if (!allowed.has(name)) {
129
+ throw new Error(`[route-convention] unsupported export "${name}" in ${filePath}`);
130
+ }
131
+ exported.add(name);
132
+ };
133
+
134
+ for (const statement of sourceFile.statements) {
135
+ if (ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement)) continue;
136
+ if (ts.isExportDeclaration(statement)) {
137
+ if (statement.isTypeOnly) continue;
138
+ const clause = statement.exportClause;
139
+ if (!clause) throw new Error(`[route-convention] export * is not allowed in route modules: ${filePath}`);
140
+ if (ts.isNamedExports(clause)) {
141
+ for (const element of clause.elements) {
142
+ if (element.isTypeOnly) continue;
143
+ assertExport(element.name.text);
144
+ }
145
+ }
146
+ continue;
147
+ }
148
+ const modifiers = ts.canHaveModifiers(statement) ? ts.getModifiers(statement) : undefined;
149
+ const isExported = modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
150
+ if (!isExported) continue;
151
+ const isDefault = modifiers?.some((m) => m.kind === ts.SyntaxKind.DefaultKeyword) ?? false;
152
+ if (isDefault) {
153
+ assertExport("default");
154
+ continue;
155
+ }
156
+ if (ts.isVariableStatement(statement)) {
157
+ for (const declaration of statement.declarationList.declarations) {
158
+ if (ts.isIdentifier(declaration.name)) assertExport(declaration.name.text);
159
+ }
160
+ continue;
161
+ }
162
+ const name = (statement as unknown as { name?: ts.Node }).name;
163
+ if (name && ts.isIdentifier(name)) {
164
+ assertExport(name.text);
165
+ }
166
+ }
167
+ if (exported.has("head") && exported.has("generateHead")) {
168
+ throw new Error(`[route-convention] head and generateHead cannot both be exported in ${filePath}`);
169
+ }
170
+ }
171
+
172
+ export class Executor {
173
+ static verbose = false;
174
+ static setVerbose(verbose: boolean) {
175
+ Executor.verbose = verbose;
176
+ }
177
+
178
+ name: string;
179
+ logger: Logger;
180
+ logs: string[];
181
+ cwdPath: string;
182
+ emoji = execEmoji.default;
183
+ typeChecker: TypeChecker | null = null;
184
+ linter: Linter | null = null;
185
+ constructor(name: string, cwdPath: string) {
186
+ this.name = name;
187
+ this.logger = new Logger(name);
188
+ this.logs = [] as string[];
189
+ this.cwdPath = cwdPath;
190
+ //! TODO: 테스트 확인 필요
191
+ // if (!(await Bun.file(cwdPath).exists())) await mkdir(cwdPath, { recursive: true });
192
+ }
193
+ #stdout(data: Buffer) {
194
+ if (Executor.verbose) Logger.raw(chalk.dim(data.toString()));
195
+ }
196
+ #stderr(data: Buffer) {
197
+ Logger.raw(chalk.red(data.toString()));
198
+ }
199
+ exec(command: string, options: ExecOptions = {}) {
200
+ const proc = exec(command, { cwd: this.cwdPath, ...options });
201
+ proc.stdout?.on("data", (data: Buffer) => {
202
+ this.#stdout(data);
203
+ });
204
+ proc.stderr?.on("data", (data: Buffer) => {
205
+ this.#stdout(data); // 정상로그도 stderr로 나옴
206
+ });
207
+ return new Promise((resolve, reject) => {
208
+ proc.on("exit", (code, signal) => {
209
+ if (!!code || signal) reject({ code, signal });
210
+ else resolve({ code, signal });
211
+ });
212
+ });
213
+ }
214
+
215
+ spawn(command: string, args: string[] = [], options: SpawnOptions = {}): Promise<string> {
216
+ const proc = spawn(command, args, {
217
+ cwd: this.cwdPath,
218
+ // stdio: "inherit",
219
+ ...options,
220
+ });
221
+ let stdout = "";
222
+ let stderr = "";
223
+
224
+ proc.stdout?.on("data", (data: Buffer) => {
225
+ stdout += data.toString();
226
+ this.logs.push(data.toString());
227
+ this.#stdout(data);
228
+ });
229
+ proc.stderr?.on("data", (data: Buffer) => {
230
+ stderr += data.toString();
231
+ this.logs.push(data.toString());
232
+ this.#stdout(data); // 정상로그도 stderr로 나옴
233
+ });
234
+ return new Promise((resolve, reject) => {
235
+ proc.on("close", (code, signal) => {
236
+ if (code !== 0 || signal) reject(stderr || stdout);
237
+ else resolve(stdout);
238
+ });
239
+ });
240
+ }
241
+ spawnSync(command: string, args: string[] = [], options: SpawnOptions = {}): ChildProcess {
242
+ const proc = spawn(command, args, {
243
+ cwd: this.cwdPath,
244
+ // stdio: "inherit",
245
+ ...options,
246
+ });
247
+ return proc;
248
+ }
249
+ fork(modulePath: string, args: string[] = [], options: ForkOptions = {}) {
250
+ const proc = fork(modulePath, args, {
251
+ cwd: this.cwdPath,
252
+ // stdio: ["ignore", "inherit", "inherit", "ipc"],
253
+ ...options,
254
+ });
255
+ proc.stdout?.on("data", (data: Buffer) => {
256
+ this.#stdout(data);
257
+ });
258
+ proc.stderr?.on("data", (data: Buffer) => {
259
+ this.#stderr(data);
260
+ });
261
+ return new Promise((resolve, reject) => {
262
+ proc.on("exit", (code, signal) => {
263
+ if (!!code || signal) reject({ code, signal });
264
+ else resolve({ code, signal });
265
+ });
266
+ });
267
+ }
268
+ getPath(filePath: string) {
269
+ if (path.isAbsolute(filePath)) return filePath;
270
+ if (filePath.startsWith(".")) return path.join(this.cwdPath, filePath);
271
+ const baseParts = this.cwdPath.split("/").filter(Boolean);
272
+ const targetParts = filePath.split("/").filter(Boolean);
273
+
274
+ let overlapLength = 0;
275
+ for (let i = 1; i <= Math.min(baseParts.length, targetParts.length); i++) {
276
+ let isOverlap = true;
277
+ for (let j = 0; j < i; j++)
278
+ if (baseParts[baseParts.length - i + j] !== targetParts[j]) {
279
+ isOverlap = false;
280
+ break;
281
+ }
282
+ if (isOverlap) overlapLength = i;
283
+ }
284
+ const result =
285
+ overlapLength > 0
286
+ ? `/${[...baseParts, ...targetParts.slice(overlapLength)].join("/")}`
287
+ : `${this.cwdPath}/${filePath}`;
288
+ return result.replace(/\/+/g, "/");
289
+ }
290
+ async mkdir(dirPath: string) {
291
+ const writePath = this.getPath(dirPath);
292
+ if (!(await FileSys.dirExists(writePath))) await mkdir(writePath, { recursive: true });
293
+ this.logger.verbose(`Make directory ${writePath}`);
294
+ return this;
295
+ }
296
+ async readdir(dirPath: string): Promise<string[]> {
297
+ const readPath = this.getPath(dirPath);
298
+ try {
299
+ const glob = new Bun.Glob("*");
300
+ return Array.from(glob.scanSync({ cwd: readPath, onlyFiles: false }));
301
+ } catch {
302
+ return [];
303
+ }
304
+ }
305
+ async getAllFiles(pattern = "**/*", { cwd }: { cwd?: string } = {}): Promise<string[]> {
306
+ const glob = new Bun.Glob(pattern);
307
+ return Array.from(glob.scanSync({ cwd: cwd ?? this.cwdPath, onlyFiles: true }));
308
+ }
309
+ async getFilesAndDirs(dirPath: string): Promise<{ files: string[]; dirs: string[] }> {
310
+ const fullDirPath = this.getPath(dirPath);
311
+ const fileGlob = new Bun.Glob("*");
312
+ const files = Array.from(fileGlob.scanSync({ cwd: fullDirPath, onlyFiles: true }));
313
+ const dirGlob = new Bun.Glob("*");
314
+ const allEntries = Array.from(dirGlob.scanSync({ cwd: fullDirPath, onlyFiles: false }));
315
+ const dirs = allEntries.filter((entry) => !files.includes(entry));
316
+ return { files, dirs };
317
+ }
318
+ async exists(filePath: string) {
319
+ const readPath = this.getPath(filePath);
320
+ return await FileSys.exists(readPath);
321
+ }
322
+ async remove(filePath: string) {
323
+ const readPath = this.getPath(filePath);
324
+ if (await FileSys.fileExists(readPath)) await FileSys.delete(readPath);
325
+ this.logger.verbose(`Remove file ${readPath}`);
326
+ return this;
327
+ }
328
+ async removeDir(dirPath: string) {
329
+ const readPath = this.getPath(dirPath);
330
+ if (await FileSys.dirExists(readPath)) await $`rm -rf ${readPath}`;
331
+ this.logger.verbose(`Remove directory ${readPath}`);
332
+ return this;
333
+ }
334
+ async writeFile(
335
+ filePath: string,
336
+ content: string | object,
337
+ { overwrite = true }: { overwrite?: boolean } = {},
338
+ ): Promise<FileContent> {
339
+ const writePath = this.getPath(filePath);
340
+ const dir = path.dirname(writePath);
341
+ if (!(await FileSys.dirExists(dir))) await mkdir(dir, { recursive: true });
342
+ let contentStr = typeof content === "string" ? content : JSON.stringify(content, null, 2);
343
+
344
+ if (await FileSys.fileExists(writePath)) {
345
+ const currentContent = await FileSys.readText(writePath);
346
+ if (currentContent === contentStr || !overwrite) {
347
+ this.logger.verbose(`File ${writePath} is unchanged`);
348
+ contentStr = currentContent;
349
+ } else {
350
+ await FileSys.writeText(writePath, contentStr);
351
+ if (Logger.isVerbose()) this.logger.rawLog(chalk.yellow(`File Update: ${filePath}`));
352
+ }
353
+ } else {
354
+ await FileSys.writeText(writePath, contentStr);
355
+ this.logger.rawLog(chalk.green(`File Create: ${filePath}`));
356
+ }
357
+ return { filePath: writePath, content: contentStr };
358
+ }
359
+ async writeJson(filePath: string, content: object) {
360
+ await this.writeFile(filePath, content);
361
+ }
362
+ async getLocalFile(targetPath: string) {
363
+ const filePath = path.isAbsolute(targetPath) ? targetPath : targetPath.replace(this.cwdPath, "");
364
+ const content = await this.readFile(filePath);
365
+ return { filePath, content };
366
+ }
367
+ async readFile(filePath: string) {
368
+ const readPath = this.getPath(filePath);
369
+ return await FileSys.readText(readPath);
370
+ }
371
+ async readJson(filePath: string) {
372
+ const readPath = this.getPath(filePath);
373
+ return await FileSys.readJson<object>(readPath);
374
+ }
375
+ async cp(srcPath: string, destPath: string) {
376
+ const src = this.getPath(srcPath);
377
+ const dest = this.getPath(destPath);
378
+ if (!(await FileSys.exists(src))) return;
379
+ const isDirectory = (await stat(src)).isDirectory();
380
+ if (!(await FileSys.exists(dest)) && isDirectory) await mkdir(dest, { recursive: true });
381
+ await $`cp -r ${src}${isDirectory ? "/." : ""} ${dest}`;
382
+ }
383
+ log(msg: string) {
384
+ this.logger.info(msg);
385
+ return this;
386
+ }
387
+ verbose(msg: string) {
388
+ this.logger.verbose(msg);
389
+ return this;
390
+ }
391
+ debug(msg: string) {
392
+ this.logger.debug(msg);
393
+ return this;
394
+ }
395
+ spinning(msg: string, { prefix = `${this.emoji}${this.name}`, indent = 0, enableSpin = !Executor.verbose } = {}) {
396
+ return new Spinner(msg, { prefix, indent, enableSpin }).start();
397
+ }
398
+
399
+ #tsconfig: TsConfigJson | null = null;
400
+ async getTsConfig(pathname = "tsconfig.json", { refresh }: { refresh?: boolean } = {}): Promise<TsConfigJson> {
401
+ if (this.#tsconfig && !refresh) return this.#tsconfig;
402
+ const tsconfig = (await this.readJson(pathname)) as TsConfigJson;
403
+ if (tsconfig.extends) {
404
+ const extendsTsconfig = await this.getTsConfig(tsconfig.extends);
405
+ const result = {
406
+ ...extendsTsconfig,
407
+ ...tsconfig,
408
+ compilerOptions: { ...extendsTsconfig.compilerOptions, ...tsconfig.compilerOptions },
409
+ } as TsConfigJson;
410
+ this.#tsconfig = result;
411
+ return result;
412
+ }
413
+ this.#tsconfig = tsconfig;
414
+ return tsconfig;
415
+ }
416
+ async setTsConfig(tsconfig: TsConfigJson) {
417
+ await this.writeJson("tsconfig.json", tsconfig);
418
+ this.#tsconfig = tsconfig;
419
+ }
420
+
421
+ #packageJson: PackageJson | null = null;
422
+ async getPackageJson({ refresh }: { refresh?: boolean } = {}): Promise<PackageJson> {
423
+ if (this.#packageJson && !refresh) return this.#packageJson;
424
+ const packageJson = (await this.readJson("package.json")) as PackageJson;
425
+ this.#packageJson = packageJson;
426
+ return packageJson;
427
+ }
428
+ async setPackageJson(packageJson: PackageJson) {
429
+ await this.writeJson("package.json", packageJson);
430
+ this.#packageJson = packageJson;
431
+ }
432
+
433
+ #gitignorePatterns: string[] = [];
434
+ async getGitignorePatterns(): Promise<string[]> {
435
+ if (this.#gitignorePatterns.length) return this.#gitignorePatterns;
436
+ const gitignore = await this.readFile(".gitignore");
437
+ this.#gitignorePatterns = gitignore
438
+ .split("\n")
439
+ .map((line) => line.trim())
440
+ .filter((line) => !!line && !line.startsWith("#"));
441
+ return this.#gitignorePatterns;
442
+ }
443
+
444
+ async #applyTemplateFile(
445
+ {
446
+ templatePath,
447
+ targetPath,
448
+ scanInfo,
449
+ overwrite = true,
450
+ }: {
451
+ templatePath: string;
452
+ targetPath: string;
453
+ scanInfo?: AppInfo | LibInfo | null;
454
+ overwrite?: boolean;
455
+ },
456
+ dict: { [key: string]: string } = {},
457
+ options: { [key: string]: any } = {},
458
+ ): Promise<FileContent | null> {
459
+ if (targetPath.endsWith(".ts") || targetPath.endsWith(".tsx")) {
460
+ const getContent = (await import(templatePath)) as {
461
+ default: (
462
+ scanInfo: AppInfo | LibInfo | null,
463
+ dict: { [key: string]: string },
464
+ options?: { [key: string]: any },
465
+ ) => Promise<string | null | { filename: string; content: string }>;
466
+ };
467
+ const result = await getContent.default(scanInfo ?? null, dict, options);
468
+ if (result === null) return null;
469
+ const filename = typeof result === "object" ? result.filename : path.basename(targetPath).replace(".js", ".ts");
470
+ const content = typeof result === "object" ? result.content : result;
471
+ const dirname = path.dirname(targetPath);
472
+ const convertedTargetPath = Object.entries(dict).reduce(
473
+ (path, [key, value]) => path.replace(new RegExp(`__${key}__`, "g"), value),
474
+ `${dirname}/${filename}`,
475
+ );
476
+ this.logger.verbose(`Apply template ${templatePath} to ${convertedTargetPath}`);
477
+ return this.writeFile(convertedTargetPath, content, { overwrite });
478
+ } else if (targetPath.endsWith(".template")) {
479
+ const content = await FileSys.readText(templatePath);
480
+ const convertedTargetPath = Object.entries(dict).reduce(
481
+ (path, [key, value]) => path.replace(new RegExp(`__${key}__`, "g"), value),
482
+ targetPath.slice(0, -9),
483
+ );
484
+ const convertedContent = Object.entries(dict).reduce(
485
+ (data, [key, value]) => data.replace(new RegExp(`<%= ${key} %>`, "g"), value),
486
+ content,
487
+ );
488
+ this.logger.verbose(`Apply template ${templatePath} to ${convertedTargetPath}`);
489
+ return this.writeFile(convertedTargetPath, convertedContent, { overwrite });
490
+ } else if (staticTemplateFileExtensions.has(path.extname(targetPath).toLowerCase())) {
491
+ const convertedTargetPath = Object.entries(dict).reduce(
492
+ (path, [key, value]) => path.replace(new RegExp(`__${key}__`, "g"), value),
493
+ targetPath,
494
+ );
495
+ const writePath = this.getPath(convertedTargetPath);
496
+ const dirname = path.dirname(writePath);
497
+ if (!(await FileSys.dirExists(dirname))) await mkdir(dirname, { recursive: true });
498
+ await copyFile(templatePath, writePath);
499
+ this.logger.verbose(`Apply template ${templatePath} to ${convertedTargetPath}`);
500
+ return { filePath: writePath, content: "" };
501
+ } else return null;
502
+ }
503
+ async _applyTemplate({
504
+ basePath,
505
+ template,
506
+ scanInfo,
507
+ dict = {},
508
+ options = {},
509
+ overwrite = true,
510
+ }: {
511
+ basePath: string;
512
+ template: string;
513
+ scanInfo?: AppInfo | LibInfo | null;
514
+ dict?: { [key: string]: string };
515
+ options?: { [key: string]: any };
516
+ overwrite?: boolean;
517
+ }): Promise<FileContent[]> {
518
+ const templateRoot = await this.#resolveTemplateRoot();
519
+ const templatePath = `${templateRoot}${template ? `/${template}` : ""}`;
520
+ const prefixTemplatePath = templatePath; // templatePath.endsWith(".tsx") ? templatePath : templatePath.replace(".ts", ".js");
521
+ if ((await stat(prefixTemplatePath)).isFile()) {
522
+ const filename = path.basename(prefixTemplatePath);
523
+ const fileContent = await this.#applyTemplateFile(
524
+ { templatePath: prefixTemplatePath, targetPath: path.join(basePath, filename), scanInfo, overwrite },
525
+ dict,
526
+ options,
527
+ );
528
+ return fileContent ? [fileContent] : ([] as FileContent[]);
529
+ } else {
530
+ const subdirs = await readDirEntries(templatePath);
531
+ const fileContents = (
532
+ await Promise.all(
533
+ subdirs.map(async (subdir) => {
534
+ const subpath = path.join(templatePath, subdir);
535
+ if ((await stat(subpath)).isFile()) {
536
+ const fileContent = await this.#applyTemplateFile(
537
+ { templatePath: subpath, targetPath: path.join(basePath, subdir), scanInfo, overwrite },
538
+ dict,
539
+ options,
540
+ );
541
+ return fileContent ? [fileContent] : ([] as FileContent[]);
542
+ } else
543
+ return await this._applyTemplate({
544
+ basePath: path.join(basePath, subdir),
545
+ template: path.join(template, subdir),
546
+ scanInfo,
547
+ dict,
548
+ overwrite,
549
+ options,
550
+ });
551
+ }),
552
+ )
553
+ ).flat();
554
+ return fileContents;
555
+ }
556
+ }
557
+ async #resolveTemplateRoot() {
558
+ const candidates = [
559
+ path.resolve(getDirname(import.meta.url), "templates"),
560
+ path.resolve(getDirname(import.meta.url), "../cli/templates"),
561
+ ];
562
+ for (const candidate of candidates) {
563
+ if (await FileSys.dirExists(candidate)) return candidate;
564
+ }
565
+ return candidates[0];
566
+ }
567
+ async applyTemplate(options: {
568
+ basePath: string;
569
+ template: string;
570
+ dict?: { [key: string]: string };
571
+ options?: { [key: string]: any };
572
+ overwrite?: boolean;
573
+ }): Promise<FileContent[]> {
574
+ const dict = {
575
+ ...(options.dict ?? {}),
576
+ ...Object.fromEntries(
577
+ Object.entries(options.dict ?? {}).map(([key, value]) => [capitalize(key), capitalize(value)]),
578
+ ),
579
+ };
580
+ return this._applyTemplate({ ...options, dict });
581
+ }
582
+ getTypeChecker() {
583
+ this.typeChecker ??= new TypeChecker(this);
584
+ return this.typeChecker;
585
+ }
586
+ typeCheck(filePath: string) {
587
+ const path = this.getPath(filePath);
588
+ const typeChecker = this.getTypeChecker();
589
+ const { fileDiagnostics, fileErrors, fileWarnings } = typeChecker.check(path);
590
+ const message = typeChecker.formatDiagnostics(fileDiagnostics);
591
+ return { fileDiagnostics, fileErrors, fileWarnings, message };
592
+ }
593
+ getLinter() {
594
+ this.linter ??= new Linter(this.cwdPath);
595
+ return this.linter;
596
+ }
597
+ async lint(
598
+ filePath: string,
599
+ { fix = false, dryRun = false }: { fix?: boolean; dryRun?: boolean } = {},
600
+ ): Promise<{
601
+ results: any[]; // ESLint.LintResult[];
602
+ message: string;
603
+ errors: any[]; // ESLintLinter.LintMessage[];
604
+ warnings: any[]; // ESLintLinter.LintMessage[];
605
+ }> {
606
+ const path = this.getPath(filePath);
607
+ const linter = this.getLinter();
608
+ const { results, errors, warnings } = await linter.lint(path, { fix, dryRun });
609
+ const message = linter.formatLintResults(results);
610
+ return { results, message, errors, warnings };
611
+ }
612
+ }
613
+
614
+ interface ExecutorOptions {
615
+ workspaceRoot: string;
616
+ repoName: string;
617
+ }
618
+ export class WorkspaceExecutor extends Executor {
619
+ workspaceRoot: string;
620
+ repoName: string;
621
+ override emoji = execEmoji.workspace;
622
+ constructor({ workspaceRoot, repoName }: ExecutorOptions) {
623
+ super("workspace", workspaceRoot);
624
+ this.workspaceRoot = workspaceRoot;
625
+ this.repoName = repoName;
626
+ }
627
+
628
+ static #execs = new Map<string, WorkspaceExecutor>();
629
+ static fromRoot({
630
+ workspaceRoot = process.cwd(),
631
+ repoName = path.basename(process.cwd()),
632
+ }: {
633
+ workspaceRoot?: string;
634
+ repoName?: string;
635
+ } = {}) {
636
+ return WorkspaceExecutor.#execs.get(repoName) ?? new WorkspaceExecutor({ workspaceRoot, repoName });
637
+ }
638
+ static getBaseDevEnv(envPath?: string) {
639
+ // Bun auto-loads .env, so we use process.env directly
640
+ const sourceEnv = envPath ? { ...process.env, ...parseEnvFile(envPath) } : process.env;
641
+
642
+ const appName = sourceEnv.AKAN_PUBLIC_APP_NAME;
643
+ const workspaceRoot = sourceEnv.AKAN_WORKSPACE_ROOT;
644
+
645
+ const repoName = sourceEnv.AKAN_PUBLIC_REPO_NAME;
646
+ if (!repoName) throw new Error("AKAN_PUBLIC_REPO_NAME is not set");
647
+
648
+ const serveDomain = sourceEnv.AKAN_PUBLIC_SERVE_DOMAIN;
649
+ if (!serveDomain) throw new Error("AKAN_PUBLIC_SERVE_DOMAIN is not set");
650
+
651
+ const portOffset = parseInt(sourceEnv.PORT_OFFSET ?? "0");
652
+
653
+ const env = (sourceEnv.AKAN_PUBLIC_ENV ?? "debug") as
654
+ | "testing"
655
+ | "debug"
656
+ | "develop"
657
+ | "main"
658
+ | "local"
659
+ | undefined;
660
+ if (!env) throw new Error("AKAN_PUBLIC_ENV is not set");
661
+ return { ...(appName ? { appName } : {}), workspaceRoot, repoName, serveDomain, env, portOffset };
662
+ }
663
+ async scan(): Promise<WorkspaceInfo> {
664
+ return await WorkspaceInfo.fromExecutor(this);
665
+ }
666
+ async getApps() {
667
+ if (!(await FileSys.dirExists(`${this.workspaceRoot}/apps`))) return [];
668
+ return await this.#getDirHasFile(`${this.workspaceRoot}/apps`, "akan.config.ts");
669
+ }
670
+ async getLibs() {
671
+ if (!(await FileSys.dirExists(`${this.workspaceRoot}/libs`))) return [];
672
+ return await this.#getDirHasFile(`${this.workspaceRoot}/libs`, "akan.config.ts");
673
+ }
674
+ async getSyss() {
675
+ const [appNames, libNames] = await Promise.all([this.getApps(), this.getLibs()]);
676
+ return [appNames, libNames] as [string[], string[]];
677
+ }
678
+ async getPkgs() {
679
+ if (!(await FileSys.dirExists(`${this.workspaceRoot}/pkgs`))) return [];
680
+ return await this.#getDirHasFile(`${this.workspaceRoot}/pkgs`, "package.json");
681
+ }
682
+ async getExecs() {
683
+ const [appNames, libNames, pkgNames] = await Promise.all([this.getApps(), this.getLibs(), this.getPkgs()]);
684
+ return [appNames, libNames, pkgNames] as [string[], string[], string[]];
685
+ }
686
+ async setPkgTsPaths(name: string) {
687
+ const rootTsConfig = (await this.readJson("tsconfig.json")) as TsConfigJson;
688
+ rootTsConfig.compilerOptions.paths ??= {};
689
+ rootTsConfig.compilerOptions.paths[name] = [`./pkgs/${name}/index.ts`];
690
+ rootTsConfig.compilerOptions.paths[`${name}/*`] = [`./pkgs/${name}/*`];
691
+ if (rootTsConfig.references) {
692
+ if (!rootTsConfig.references.some((ref) => ref.path === `./pkgs/${name}/tsconfig.json`))
693
+ rootTsConfig.references.push({ path: `./pkgs/${name}/tsconfig.json` });
694
+ }
695
+ await this.writeJson("tsconfig.json", rootTsConfig);
696
+ return this;
697
+ }
698
+ async unsetPkgTsPaths(name: string) {
699
+ const rootTsConfig = (await this.readJson("tsconfig.json")) as TsConfigJson;
700
+ const filteredKeys = Object.keys(rootTsConfig.compilerOptions.paths ?? {}).filter(
701
+ (key) => key !== name && key !== `${name}/*`,
702
+ );
703
+ rootTsConfig.compilerOptions.paths = Object.fromEntries(
704
+ filteredKeys.map((key) => [key, rootTsConfig.compilerOptions.paths?.[key] ?? []]),
705
+ );
706
+ if (rootTsConfig.references) {
707
+ rootTsConfig.references = rootTsConfig.references.filter(
708
+ (ref) => ref.path !== `./pkgs/${name}/tsconfig.json`,
709
+ ) as TsConfigJson["references"];
710
+ }
711
+ await this.writeJson("tsconfig.json", rootTsConfig);
712
+ return this;
713
+ }
714
+ async getDirInModule(basePath: string, name: string) {
715
+ const AVOID_DIRS = ["__lib", "__scalar", `_`, `_${name}`];
716
+ const getDirs = async (dirname: string, maxDepth = 3, results: string[] = [], prefix = "") => {
717
+ const dirs = await this.readdir(dirname);
718
+ await Promise.all(
719
+ dirs.map(async (dir) => {
720
+ if (dir.includes("_") || AVOID_DIRS.includes(dir)) return;
721
+ const dirPath = path.join(dirname, dir);
722
+ if ((await stat(dirPath)).isDirectory()) {
723
+ results.push(`${prefix}${dir}`);
724
+ if (maxDepth > 0) await getDirs(dirPath, maxDepth - 1, results, `${prefix}${dir}/`);
725
+ }
726
+ }),
727
+ );
728
+ return results;
729
+ };
730
+ return await getDirs(basePath);
731
+ }
732
+ async commit(message: string, { init = false, add = true }: { init?: boolean; add?: boolean } = {}) {
733
+ if (init) await this.exec(`git init --quiet`);
734
+ if (add) await this.exec(`git add .`);
735
+ await this.exec(`git commit --quiet -m "${message}"`);
736
+ }
737
+ async #getDirHasFile(basePath: string, targetFilename: string) {
738
+ const AVOID_DIRS = ["node_modules", "dist", "public", "webkit"];
739
+ const getDirs = async (dirname: string, maxDepth = 3, results: string[] = [], prefix = "") => {
740
+ const dirs = await this.readdir(dirname);
741
+ await Promise.all(
742
+ dirs.map(async (dir) => {
743
+ if (AVOID_DIRS.includes(dir)) return;
744
+ const dirPath = path.join(dirname, dir);
745
+ if ((await stat(dirPath)).isDirectory()) {
746
+ const hasTargetFile = await FileSys.fileExists(path.join(dirPath, targetFilename));
747
+ if (hasTargetFile) results.push(`${prefix}${dir}`);
748
+ if (maxDepth > 0) await getDirs(dirPath, maxDepth - 1, results, `${prefix}${dir}/`);
749
+ }
750
+ }),
751
+ );
752
+ return results;
753
+ };
754
+ return await getDirs(basePath);
755
+ }
756
+
757
+ async getScalarConstantFiles() {
758
+ const [appNames, libNames] = await this.getSyss();
759
+ const scalarConstantExampleFiles = [
760
+ ...(
761
+ await Promise.all(appNames.map((appName) => AppExecutor.from(this, appName).getScalarConstantFiles()))
762
+ ).flat(),
763
+ ...(
764
+ await Promise.all(libNames.map((libName) => LibExecutor.from(this, libName).getScalarConstantFiles()))
765
+ ).flat(),
766
+ ];
767
+ return scalarConstantExampleFiles;
768
+ }
769
+ async getConstantFiles() {
770
+ const [appNames, libNames] = await this.getSyss();
771
+ const moduleConstantExampleFiles = [
772
+ ...(await Promise.all(appNames.map((appName) => AppExecutor.from(this, appName).getConstantFiles()))).flat(),
773
+ ...(await Promise.all(libNames.map((libName) => LibExecutor.from(this, libName).getConstantFiles()))).flat(),
774
+ ];
775
+ return moduleConstantExampleFiles;
776
+ }
777
+ async getDictionaryFiles() {
778
+ const [appNames, libNames] = await this.getSyss();
779
+ const moduleDictionaryExampleFiles = [
780
+ ...(await Promise.all(appNames.map((appName) => AppExecutor.from(this, appName).getDictionaryFiles()))).flat(),
781
+ ...(await Promise.all(libNames.map((libName) => LibExecutor.from(this, libName).getDictionaryFiles()))).flat(),
782
+ ];
783
+ return moduleDictionaryExampleFiles;
784
+ }
785
+ async getViewFiles() {
786
+ const [appNames, libNames] = await this.getSyss();
787
+ const viewExampleFiles = [
788
+ ...(await Promise.all(appNames.map((appName) => AppExecutor.from(this, appName).getViewsSourceCode()))).flat(),
789
+ ...(await Promise.all(libNames.map((libName) => LibExecutor.from(this, libName).getViewsSourceCode()))).flat(),
790
+ ];
791
+ return viewExampleFiles;
792
+ }
793
+ }
794
+
795
+ interface SysExecutorOptions {
796
+ workspace?: WorkspaceExecutor;
797
+ name: string;
798
+ type: "app" | "lib";
799
+ }
800
+
801
+ const scanFacetDirs = ["ui", "webkit", "srvkit", "common"] as const;
802
+
803
+ export class SysExecutor extends Executor {
804
+ workspace: WorkspaceExecutor;
805
+ override name: string;
806
+ type: "app" | "lib";
807
+ override emoji: string;
808
+ constructor({ workspace = WorkspaceExecutor.fromRoot(), name, type }: SysExecutorOptions) {
809
+ super(name, `${workspace.workspaceRoot}/${type}s/${name}`);
810
+ this.workspace = workspace;
811
+ this.name = name;
812
+ this.type = type;
813
+ this.emoji = execEmoji[type];
814
+ }
815
+ #akanConfig: AkanAppConfig | AkanLibConfig | null = null;
816
+ async getConfig({ refresh }: { refresh?: boolean } = {}) {
817
+ if (this.#akanConfig && !refresh) return this.#akanConfig;
818
+ this.#akanConfig =
819
+ this.type === "app"
820
+ ? await AkanAppConfig.from(this as unknown as AppExecutor)
821
+ : await AkanLibConfig.from(this as unknown as LibExecutor);
822
+ return this.#akanConfig;
823
+ }
824
+ async getModules() {
825
+ const path = this.type === "app" ? `apps/${this.name}/lib` : `libs/${this.name}/lib`;
826
+ return await this.workspace.getDirInModule(path, this.name);
827
+ }
828
+
829
+ #scanInfo: AppInfo | LibInfo | null = null;
830
+ hasScanInfo() {
831
+ return this.#scanInfo !== null;
832
+ }
833
+ getScanInfo<AllowEmpty extends boolean = false>({
834
+ allowEmpty,
835
+ }: {
836
+ allowEmpty?: AllowEmpty;
837
+ } = {}): AllowEmpty extends true ? AppInfo | LibInfo | null : AppInfo | LibInfo {
838
+ if (!this.hasScanInfo() && !allowEmpty) throw new Error("Scan info is not available");
839
+ return this.#scanInfo as AllowEmpty extends true ? AppInfo | LibInfo | null : AppInfo | LibInfo;
840
+ }
841
+ #getScanTemplateTasks(scanInfo: AppInfo | LibInfo): (Promise<FileContent[]> | null)[] {
842
+ return [
843
+ this._applyTemplate({ basePath: "env", template: "env", scanInfo }),
844
+ this._applyTemplate({ basePath: "lib", template: "lib", scanInfo }),
845
+ this._applyTemplate({ basePath: ".", template: "server.ts", scanInfo }),
846
+ this._applyTemplate({ basePath: ".", template: "client.ts", scanInfo }),
847
+ this.type === "lib" ? this._applyTemplate({ basePath: ".", template: "index.ts", scanInfo }) : null,
848
+ ...scanFacetDirs.map((facet) =>
849
+ this._applyTemplate({
850
+ basePath: facet,
851
+ template: "facetIndex/index.ts",
852
+ scanInfo,
853
+ options: { exec: this, facet },
854
+ }),
855
+ ),
856
+ ...scanInfo.getDatabaseModules().map((model) =>
857
+ this._applyTemplate({
858
+ basePath: `lib/${model}`,
859
+ template: "moduleRoot",
860
+ scanInfo,
861
+ dict: { model, Model: capitalize(model) },
862
+ }),
863
+ ),
864
+ ...scanInfo.getServiceModules().map((model) =>
865
+ this._applyTemplate({
866
+ basePath: `lib/_${model}`,
867
+ template: "moduleRoot",
868
+ scanInfo,
869
+ dict: { model, Model: capitalize(model) },
870
+ }),
871
+ ),
872
+ ...scanInfo.getScalarModules().map((model) =>
873
+ this._applyTemplate({
874
+ basePath: `lib/__scalar/${model}`,
875
+ template: "moduleRoot",
876
+ scanInfo,
877
+ dict: { model, Model: capitalize(model) },
878
+ }),
879
+ ),
880
+ ];
881
+ }
882
+ async scan({
883
+ refresh = false,
884
+ write = true,
885
+ writeLib = write,
886
+ }: {
887
+ refresh?: boolean;
888
+ write?: boolean;
889
+ writeLib?: boolean;
890
+ } = {}): Promise<AppInfo | LibInfo> {
891
+ if (this.#scanInfo && !refresh) return this.#scanInfo;
892
+ const scanInfo =
893
+ this.type === "app"
894
+ ? await AppInfo.fromExecutor(this as unknown as AppExecutor, { refresh })
895
+ : await LibInfo.fromExecutor(this as unknown as LibExecutor, { refresh });
896
+ if (write) {
897
+ await Promise.all(this.#getScanTemplateTasks(scanInfo));
898
+ await this.writeJson(`akan.${this.type}.json`, scanInfo.getScanResult());
899
+ if (this.type === "lib") this.#updateDependencies(scanInfo);
900
+
901
+ if (writeLib) {
902
+ const libInfos = [...scanInfo.getLibInfos().values()];
903
+ await this.#updateDependencies(scanInfo);
904
+ await Promise.all(libInfos.flatMap((libInfo) => libInfo.exec.#getScanTemplateTasks(libInfo)));
905
+ }
906
+ }
907
+ this.#scanInfo = scanInfo;
908
+ return scanInfo;
909
+ }
910
+ async #updateDependencies(scanInfo: AppInfo | LibInfo) {
911
+ const rootPackageJson = await this.workspace.getPackageJson();
912
+ const libPackageJson = await this.getPackageJson();
913
+ const dependencies = scanInfo.getScanResult().dependencies;
914
+ const devDependencies = scanInfo.getScanResult().devDependencies;
915
+ const dependencySet = new Set(dependencies);
916
+ const devDependencySet = new Set(devDependencies);
917
+ const libPkgJsonWithDeps: PackageJson = {
918
+ ...libPackageJson,
919
+ dependencies: {
920
+ ...Object.fromEntries(
921
+ Object.entries(libPackageJson.dependencies ?? {}).filter(([dep]) => !devDependencySet.has(dep)),
922
+ ),
923
+ ...(Object.fromEntries(
924
+ dependencies
925
+ .filter((dep) => rootPackageJson.dependencies?.[dep])
926
+ .sort()
927
+ .map((dep) => [dep, rootPackageJson.dependencies?.[dep]]),
928
+ ) as Record<string, string>),
929
+ },
930
+ devDependencies: {
931
+ ...Object.fromEntries(
932
+ Object.entries(libPackageJson.devDependencies ?? {}).filter(([dep]) => !dependencySet.has(dep)),
933
+ ),
934
+ ...(Object.fromEntries(
935
+ devDependencies
936
+ .filter((dep) => rootPackageJson.dependencies?.[dep] || rootPackageJson.devDependencies?.[dep])
937
+ .sort()
938
+ .map((dep) => [dep, rootPackageJson.devDependencies?.[dep] ?? rootPackageJson.dependencies?.[dep]]),
939
+ ) as Record<string, string>),
940
+ },
941
+ };
942
+ await this.setPackageJson(libPkgJsonWithDeps);
943
+ }
944
+ override async getLocalFile(targetPath: string) {
945
+ const filePath = path.isAbsolute(targetPath) ? targetPath : `${this.type}s/${this.name}/${targetPath}`;
946
+ const content = await this.workspace.readFile(filePath);
947
+ return { filePath, content };
948
+ }
949
+
950
+ async getDatabaseModules() {
951
+ const databaseModules = (await this.readdir("lib"))
952
+ .filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts"))
953
+ .filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${name}.constant.ts`).exists());
954
+ return databaseModules;
955
+ }
956
+
957
+ async getServiceModules() {
958
+ const serviceModules = (await this.readdir("lib"))
959
+ .filter((name) => name.startsWith("_") && !name.startsWith("__"))
960
+ .filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${name}.service.ts`).exists());
961
+ return serviceModules;
962
+ }
963
+
964
+ async getScalarModules() {
965
+ const scalarModules = (await this.readdir("lib/__scalar"))
966
+ .filter((name) => !name.startsWith("_"))
967
+ .filter((name) => Bun.file(`${this.cwdPath}/lib/__scalar/${name}/${name}.constant.ts`).exists());
968
+ return scalarModules;
969
+ }
970
+
971
+ async getViewComponents() {
972
+ const viewComponents = (await this.readdir("lib"))
973
+ .filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts"))
974
+ .filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${name}.View.tsx`).exists());
975
+ return viewComponents;
976
+ }
977
+
978
+ async getUnitComponents() {
979
+ const unitComponents = (await this.readdir("lib"))
980
+ .filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts"))
981
+ .filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${name}.Unit.tsx`).exists());
982
+ return unitComponents;
983
+ }
984
+ async getTemplateComponents() {
985
+ const templateComponents = (await this.readdir("lib"))
986
+ .filter((name) => !name.startsWith("_") && !name.startsWith("__") && !name.endsWith(".ts"))
987
+ .filter((name) => Bun.file(`${this.cwdPath}/lib/${name}/${name}.Template.tsx`).exists());
988
+ return templateComponents;
989
+ }
990
+
991
+ async getViewsSourceCode() {
992
+ const viewComponents = await this.getViewComponents();
993
+ return Promise.all(
994
+ viewComponents.map((viewComponent) => this.getLocalFile(`lib/${viewComponent}/${viewComponent}.View.tsx`)),
995
+ );
996
+ }
997
+ async getUnitsSourceCode() {
998
+ const unitComponents = await this.getUnitComponents();
999
+ return Promise.all(
1000
+ unitComponents.map((unitComponent) => this.getLocalFile(`lib/${unitComponent}/${unitComponent}.Unit.tsx`)),
1001
+ );
1002
+ }
1003
+ async getTemplatesSourceCode() {
1004
+ const templateComponents = await this.getTemplateComponents();
1005
+ return Promise.all(
1006
+ templateComponents.map((templateComponent) =>
1007
+ this.getLocalFile(`lib/${templateComponent}/${templateComponent}.Template.tsx`),
1008
+ ),
1009
+ );
1010
+ }
1011
+
1012
+ async getScalarConstantFiles() {
1013
+ const scalarModules = await this.getScalarModules();
1014
+ return Promise.all(
1015
+ scalarModules.map((scalarModule) =>
1016
+ this.getLocalFile(`lib/__scalar/${scalarModule}/${scalarModule}.constant.ts`),
1017
+ ),
1018
+ );
1019
+ }
1020
+
1021
+ async getScalarDictionaryFiles() {
1022
+ const scalarModules = await this.getScalarModules();
1023
+ return Promise.all(
1024
+ scalarModules.map((scalarModule) => this.getLocalFile(`lib/${scalarModule}/${scalarModule}.dictionary.ts`)),
1025
+ );
1026
+ }
1027
+
1028
+ async getConstantFiles() {
1029
+ const modules = await this.getModules();
1030
+ return Promise.all(modules.map((module) => this.getLocalFile(`lib/${module}/${module}.constant.ts`)));
1031
+ }
1032
+ async getConstantFilesWithLibs() {
1033
+ const scanInfo =
1034
+ this.type === "app"
1035
+ ? await AppInfo.fromExecutor(this as unknown as AppExecutor)
1036
+ : await LibInfo.fromExecutor(this as unknown as LibExecutor);
1037
+ const sysContantFiles = await this.getConstantFiles();
1038
+ const sysScalarConstantFiles = await this.getScalarConstantFiles();
1039
+ const libDeps = scanInfo.getLibs();
1040
+ const libConstantFiles = await Promise.all(
1041
+ libDeps.map(async (lib) => [
1042
+ ...(await LibExecutor.from(this, lib).getConstantFiles()),
1043
+ ...(await LibExecutor.from(this, lib).getScalarConstantFiles()),
1044
+ ]),
1045
+ );
1046
+ return [...sysContantFiles, ...sysScalarConstantFiles, ...libConstantFiles.flat()];
1047
+ }
1048
+ async getDictionaryFiles() {
1049
+ const modules = await this.getModules();
1050
+ return Promise.all(modules.map((module) => this.getLocalFile(`lib/${module}/${module}.dictionary.ts`)));
1051
+ }
1052
+ override async applyTemplate(options: {
1053
+ basePath: string;
1054
+ template: string;
1055
+ dict?: { [key: string]: string };
1056
+ overwrite?: boolean;
1057
+ }): Promise<FileContent[]> {
1058
+ const dict = {
1059
+ ...(options.dict ?? {}),
1060
+ ...Object.fromEntries(
1061
+ Object.entries(options.dict ?? {}).map(([key, value]) => [capitalize(key), capitalize(value)]),
1062
+ ),
1063
+ };
1064
+ const scanInfo = await this.scan();
1065
+ const fileContents = await this._applyTemplate({ ...options, scanInfo, dict });
1066
+ await this.scan();
1067
+ return fileContents;
1068
+ }
1069
+ }
1070
+
1071
+ interface AppExecutorOptions {
1072
+ workspace?: WorkspaceExecutor;
1073
+ name: string;
1074
+ }
1075
+ export class AppExecutor extends SysExecutor {
1076
+ dist: Executor;
1077
+ override emoji = execEmoji.app;
1078
+ constructor({ workspace, name }: AppExecutorOptions) {
1079
+ super({ workspace, name, type: "app" });
1080
+ this.dist = new Executor(`dist/${name}`, `${this.workspace.workspaceRoot}/dist/apps/${name}`);
1081
+ }
1082
+ static #execs = new Map<string, AppExecutor>();
1083
+ static from(executor: SysExecutor | WorkspaceExecutor, name: string) {
1084
+ const exec = AppExecutor.#execs.get(name);
1085
+ if (exec) return exec;
1086
+ else if (executor instanceof WorkspaceExecutor) return new AppExecutor({ workspace: executor, name });
1087
+ else return new AppExecutor({ workspace: executor.workspace, name });
1088
+ }
1089
+ getEnv() {
1090
+ return WorkspaceExecutor.getBaseDevEnv().env;
1091
+ }
1092
+ getCommandEnv(env: Record<string, string> = {}): Record<string, string> {
1093
+ const basePort = 8282;
1094
+ const portOffset = WorkspaceExecutor.getBaseDevEnv().portOffset;
1095
+ const PORT = basePort ? (basePort + portOffset).toString() : undefined;
1096
+ const AKAN_PUBLIC_SERVER_PORT = portOffset ? (8282 + portOffset).toString() : undefined;
1097
+ return {
1098
+ ...process.env,
1099
+ AKAN_PUBLIC_APP_NAME: this.name,
1100
+ AKAN_WORKSPACE_ROOT: this.workspace.workspaceRoot,
1101
+ NODE_NO_WARNINGS: "1",
1102
+ ...(PORT ? { PORT, AKAN_PUBLIC_CLIENT_PORT: PORT } : {}),
1103
+ ...(AKAN_PUBLIC_SERVER_PORT ? { AKAN_PUBLIC_SERVER_PORT } : {}),
1104
+ ...env,
1105
+ };
1106
+ }
1107
+ async prepareCommand(type: "build" | "start") {
1108
+ const akanConfig = await this.getConfig();
1109
+ const databaseMode = process.env.AKAN_DATABASE_MODE ?? akanConfig.defaultDatabaseMode ?? "single";
1110
+ const routeEnv = {
1111
+ AKAN_PUBLIC_BASE_PATHS: [...akanConfig.basePaths].join(","),
1112
+ AKAN_DATABASE_MODE: databaseMode,
1113
+ };
1114
+ Object.assign(process.env, routeEnv);
1115
+ if (type === "build") {
1116
+ if (await this.exists(this.dist.cwdPath)) await this.dist.exec(`rm -rf ${this.dist.cwdPath}`);
1117
+ await Promise.all([this.dist.mkdir("private"), this.dist.mkdir("public")]);
1118
+ await Promise.all([
1119
+ this.cp("private", `${this.dist.cwdPath}/private`),
1120
+ this.cp("public", `${this.dist.cwdPath}/public`),
1121
+ ]);
1122
+ } else await this.removeDir(".akan");
1123
+ const env = this.getCommandEnv({ AKAN_COMMAND_TYPE: type, ...routeEnv });
1124
+ return { env };
1125
+ }
1126
+ #publicEnv: Record<string, string> | null = null;
1127
+ getPublicEnv(...patterns: string[]) {
1128
+ if (this.#publicEnv) return this.#publicEnv;
1129
+ const searchPatterns = [...patterns, "AKAN_PUBLIC_*"];
1130
+ const regexes = searchPatterns.map((pattern) => {
1131
+ let body = "";
1132
+ for (const ch of pattern) {
1133
+ if (ch === "*") body += ".*";
1134
+ else body += ch.replace(/[.+^${}()|[\]\\?]/g, "\\$&");
1135
+ }
1136
+ return new RegExp(`^${body}$`);
1137
+ });
1138
+ const publicEnv: Record<string, string> = {};
1139
+ for (const [k, v] of Object.entries(process.env)) {
1140
+ if (typeof v !== "string") continue;
1141
+ if (regexes.some((r) => r.test(k))) publicEnv[k] = v;
1142
+ }
1143
+ this.#publicEnv = publicEnv;
1144
+ return publicEnv;
1145
+ }
1146
+
1147
+ #akanConfig: AkanAppConfig | null = null;
1148
+ override async getConfig({ refresh }: { refresh?: boolean } = {}) {
1149
+ if (this.#akanConfig && !refresh) return this.#akanConfig;
1150
+ this.#akanConfig = await AkanAppConfig.from(this);
1151
+ return this.#akanConfig;
1152
+ }
1153
+
1154
+ #pageKeys: string[] | null = null;
1155
+ async getPageKeys({ refresh }: { refresh?: boolean } = {}): Promise<string[]> {
1156
+ if (this.#pageKeys && !refresh) return this.#pageKeys;
1157
+ const akanConfig = await this.getConfig();
1158
+ const glob = new Bun.Glob("**/*");
1159
+ const pageKeys: string[] = [];
1160
+ const pageDir = `${this.cwdPath}/page`;
1161
+ if (!(await FileSys.dirExists(pageDir))) {
1162
+ this.#pageKeys = [];
1163
+ return this.#pageKeys;
1164
+ }
1165
+ for await (const rel of glob.scan({ cwd: pageDir, absolute: false, onlyFiles: true })) {
1166
+ const segments = rel.split(path.sep);
1167
+ if (segments.some((s) => s === "node_modules")) continue;
1168
+ const posix = segments.join("/");
1169
+ const absPath = path.join(pageDir, posix);
1170
+ validatePageSourceFile(posix, { filePath: absPath });
1171
+ if (!isRouteSourceFile(posix)) continue;
1172
+ const key = `./${posix}`;
1173
+ validateSubRoutePageKey(key, akanConfig.basePaths, { appName: this.name, filePath: absPath });
1174
+ const parsed = parseRouteModuleKey(key);
1175
+ if (parsed.isInternalRootLayout) {
1176
+ throw new Error(`[route-convention] __root_layout is reserved for Akan.js generated root layout: ${absPath}`);
1177
+ }
1178
+ const isRootLayout = parsed.kind === "layout" && parsed.moduleSegments.at(-1) === "_layout";
1179
+ validateRouteSourceExports(await Bun.file(absPath).text(), absPath, parsed.kind, { rootLayout: isRootLayout });
1180
+ pageKeys.push(key);
1181
+ }
1182
+ pageKeys.sort();
1183
+ this.#pageKeys = pageKeys;
1184
+ return this.#pageKeys;
1185
+ }
1186
+ setPageKeys(pageKeys: string[]) {
1187
+ this.#pageKeys = pageKeys;
1188
+ }
1189
+
1190
+ async syncAssets(libDeps: string[]) {
1191
+ const projectPublicPath = `${this.cwdPath}/public`;
1192
+ const projectAssetsPath = `${this.cwdPath}/private`;
1193
+ const projectPublicLibPath = `${projectPublicPath}/libs`;
1194
+ const projectAssetsLibPath = `${projectAssetsPath}/libs`;
1195
+ await Promise.all([this.removeDir(projectPublicLibPath), this.removeDir(projectAssetsLibPath)]);
1196
+ const targetPublicDeps = [] as string[];
1197
+ for (const dep of libDeps) {
1198
+ if (await this.exists(`${this.workspace.workspaceRoot}/libs/${dep}/public`)) targetPublicDeps.push(dep);
1199
+ }
1200
+ const targetAssetsDeps = [] as string[];
1201
+ for (const dep of libDeps) {
1202
+ if (await this.exists(`${this.workspace.workspaceRoot}/libs/${dep}/private`)) targetAssetsDeps.push(dep);
1203
+ }
1204
+ await Promise.all(targetPublicDeps.map((dep) => this.mkdir(`${projectPublicLibPath}/${dep}`)));
1205
+ await Promise.all(targetAssetsDeps.map((dep) => this.mkdir(`${projectAssetsLibPath}/${dep}`)));
1206
+ await Promise.all([
1207
+ ...targetPublicDeps.map((dep) =>
1208
+ this.cp(`${this.workspace.workspaceRoot}/libs/${dep}/public`, `${projectPublicLibPath}/${dep}`),
1209
+ ),
1210
+ ...targetAssetsDeps.map((dep) =>
1211
+ this.cp(`${this.workspace.workspaceRoot}/libs/${dep}/private`, `${projectAssetsLibPath}/${dep}`),
1212
+ ),
1213
+ ]);
1214
+ }
1215
+ async scanSync({ refresh = false, write = true }: { refresh?: boolean; write?: boolean } = {}) {
1216
+ const scanInfo = (await this.scan({ refresh, write, writeLib: write })) as AppInfo;
1217
+ if (write) await this.syncAssets(scanInfo.getScanResult().libDeps);
1218
+ return scanInfo;
1219
+ }
1220
+ async increaseBuildNum() {
1221
+ await increaseBuildNum(this);
1222
+ }
1223
+ async decreaseBuildNum() {
1224
+ await decreaseBuildNum(this);
1225
+ }
1226
+ }
1227
+ interface LibExecutorOptions {
1228
+ workspace?: WorkspaceExecutor;
1229
+ name: string;
1230
+ }
1231
+ export class LibExecutor extends SysExecutor {
1232
+ dist: Executor;
1233
+ override emoji = execEmoji.lib;
1234
+ constructor({ workspace, name }: LibExecutorOptions) {
1235
+ super({ workspace, name, type: "lib" });
1236
+ this.dist = new Executor(`dist/${name}`, `${this.workspace.workspaceRoot}/dist/libs/${name}`);
1237
+ }
1238
+ static #execs = new Map<string, LibExecutor>();
1239
+ static from(executor: SysExecutor | WorkspaceExecutor, name: string) {
1240
+ const exec = LibExecutor.#execs.get(name);
1241
+ if (exec) return exec;
1242
+ else if (executor instanceof WorkspaceExecutor) return new LibExecutor({ workspace: executor, name });
1243
+ else return new LibExecutor({ workspace: executor.workspace, name });
1244
+ }
1245
+
1246
+ #akanConfig: AkanLibConfig | null = null;
1247
+ override async getConfig({ refresh }: { refresh?: boolean } = {}) {
1248
+ if (this.#akanConfig && !refresh) return this.#akanConfig;
1249
+ this.#akanConfig = await AkanLibConfig.from(this);
1250
+ return this.#akanConfig;
1251
+ }
1252
+ }
1253
+
1254
+ interface PkgExecutorOptions {
1255
+ workspace?: WorkspaceExecutor;
1256
+ name: string;
1257
+ }
1258
+ export class PkgExecutor extends Executor {
1259
+ workspace: WorkspaceExecutor;
1260
+ override name: string;
1261
+ dist: Executor;
1262
+ override emoji = execEmoji.pkg;
1263
+ constructor({ workspace = WorkspaceExecutor.fromRoot(), name }: PkgExecutorOptions) {
1264
+ super(name, `${workspace.workspaceRoot}/pkgs/${name}`);
1265
+ this.workspace = workspace;
1266
+ this.name = name;
1267
+ this.dist = new Executor(`dist/${name}`, `${this.workspace.workspaceRoot}/dist/pkgs/${name}`);
1268
+ }
1269
+ static from(executor: SysExecutor | WorkspaceExecutor, name: string) {
1270
+ if (executor instanceof WorkspaceExecutor) return new PkgExecutor({ workspace: executor, name });
1271
+ return new PkgExecutor({ workspace: executor.workspace, name });
1272
+ }
1273
+
1274
+ #scanInfo: PkgInfo | null = null;
1275
+ async scan({ refresh }: { refresh?: boolean } = {}): Promise<PkgInfo> {
1276
+ if (this.#scanInfo && !refresh) return this.#scanInfo;
1277
+ const scanInfo = await PkgInfo.fromExecutor(this, { refresh });
1278
+ // this.writeJson("akan.pkg.json", pkgScanResult);
1279
+ this.#scanInfo = scanInfo;
1280
+ return scanInfo;
1281
+ }
1282
+ async #getDependencyVersion(rootPackageJson: PackageJson, dep: string): Promise<string | undefined> {
1283
+ const rootDeps = { ...rootPackageJson.dependencies, ...rootPackageJson.devDependencies };
1284
+ const rootVersion = rootDeps[dep];
1285
+ if (rootVersion) return rootVersion;
1286
+
1287
+ try {
1288
+ const packageJsonPath = `pkgs/${dep}/package.json`;
1289
+ if (!(await Bun.file(path.join(this.workspace.workspaceRoot, packageJsonPath)).exists())) return undefined;
1290
+ const packageJson = await this.workspace.readJson(packageJsonPath);
1291
+ if ((packageJson as PackageJson).name === dep) return (packageJson as PackageJson).version;
1292
+ } catch {
1293
+ return undefined;
1294
+ }
1295
+ }
1296
+ async #toDependencyMap(
1297
+ rootPackageJson: PackageJson,
1298
+ dependencies: string[] = [],
1299
+ devDependencies: string[] = [],
1300
+ ): Promise<Pick<PackageJson, "dependencies" | "devDependencies">> {
1301
+ const dependencyNames = [...new Set(dependencies)].sort();
1302
+ const devDependencyNames = [...new Set(devDependencies)].filter((dep) => !dependencyNames.includes(dep)).sort();
1303
+ const dependencyVersions = new Map<string, string>();
1304
+ const missingDeps: string[] = [];
1305
+ for (const dep of [...dependencyNames, ...devDependencyNames]) {
1306
+ const version = await this.#getDependencyVersion(rootPackageJson, dep);
1307
+ if (version) dependencyVersions.set(dep, version);
1308
+ else missingDeps.push(dep);
1309
+ }
1310
+ if (missingDeps.length > 0)
1311
+ throw new Error(`Missing dependency versions in root package.json: ${missingDeps.join(", ")}`);
1312
+
1313
+ const toDependencyEntries = (names: string[]) =>
1314
+ names.map((dep) => {
1315
+ const version = dependencyVersions.get(dep);
1316
+ if (!version) throw new Error(`Missing dependency versions in root package.json: ${dep}`);
1317
+ return [dep, version] as const;
1318
+ });
1319
+
1320
+ return {
1321
+ dependencies: Object.fromEntries(toDependencyEntries(dependencyNames)),
1322
+ devDependencies: Object.fromEntries(toDependencyEntries(devDependencyNames)),
1323
+ };
1324
+ }
1325
+ async updatePackageJsonDependencies(
1326
+ dependencies: string[] = [],
1327
+ devDependencies: string[] = [],
1328
+ ): Promise<PackageJson> {
1329
+ const [rootPackageJson, pkgJson] = await Promise.all([this.workspace.getPackageJson(), this.getPackageJson()]);
1330
+ const dependencyMaps = await this.#toDependencyMap(rootPackageJson, dependencies, devDependencies);
1331
+ const newPkgJson = {
1332
+ ...pkgJson,
1333
+ ...dependencyMaps,
1334
+ };
1335
+ await this.writeJson("package.json", newPkgJson);
1336
+ return newPkgJson;
1337
+ }
1338
+ async generateDistPackageJson(dependencies: string[] = [], devDependencies: string[] = []): Promise<PackageJson> {
1339
+ const [rootPackageJson, pkgJson] = await Promise.all([this.workspace.getPackageJson(), this.getPackageJson()]);
1340
+ const dependencyMaps = await this.#toDependencyMap(rootPackageJson, dependencies, devDependencies);
1341
+ const distPkgJson: PackageJson = {
1342
+ ...pkgJson,
1343
+ type: "module",
1344
+ exports: { ...pkgJson.exports, ".": { import: "./index.ts", types: "./index.ts", default: "./index.ts" } },
1345
+ engines: { bun: ">=1.3.13" },
1346
+ ...dependencyMaps,
1347
+ };
1348
+ await Promise.all([this.dist.writeJson("package.json", distPkgJson), this.writeJson("package.json", distPkgJson)]);
1349
+ return distPkgJson;
1350
+ }
1351
+ async build(): Promise<void> {
1352
+ await Bun.build({
1353
+ root: this.cwdPath,
1354
+ entrypoints: [`${this.cwdPath}/index.ts`],
1355
+ splitting: false,
1356
+ target: "bun",
1357
+ });
1358
+ await this.cp(`${this.cwdPath}/dist`, this.dist.cwdPath);
1359
+ }
1360
+ async generateTsconfigJson(): Promise<TsConfigJson> {
1361
+ const [rootTsconfig, pkgTsconfig] = await Promise.all([this.workspace.getTsConfig(), this.getTsConfig()]);
1362
+ const tsconfig: TsConfigJson = {
1363
+ ...rootTsconfig,
1364
+ ...pkgTsconfig,
1365
+ compilerOptions: {
1366
+ ...rootTsconfig.compilerOptions,
1367
+ ...pkgTsconfig.compilerOptions,
1368
+ paths: {},
1369
+ },
1370
+ };
1371
+ await this.dist.writeJson("tsconfig.json", tsconfig);
1372
+ return tsconfig;
1373
+ }
1374
+ }
1375
+
1376
+ interface ModuleExecutorOptions {
1377
+ sys: SysExecutor;
1378
+ name: string;
1379
+ }
1380
+ export class ModuleExecutor extends Executor {
1381
+ sys: SysExecutor;
1382
+ override emoji = execEmoji.module;
1383
+ constructor({ sys, name }: ModuleExecutorOptions) {
1384
+ super(name, `${sys.workspace.workspaceRoot}/${sys.type}s/${sys.name}/lib/${name}`);
1385
+ this.sys = sys;
1386
+ }
1387
+ static from(sysExecutor: SysExecutor, name: string) {
1388
+ return new ModuleExecutor({ sys: sysExecutor, name });
1389
+ }
1390
+ }