@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,303 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import type { SsrManifest, SsrManifestEntry } from "akanjs/server";
4
+ import type { BunPlugin } from "bun";
5
+ import { toClientReferencePath } from "../transforms/rscUseClientTransform";
6
+ import {
7
+ type BundleClientEntriesInternalOptions,
8
+ type BundleClientEntriesResult,
9
+ CLIENT_BUNDLE_NAMING,
10
+ type ClientManifest,
11
+ type MetafileOutput,
12
+ type OpaqueEntryAliases,
13
+ } from "./clientBuildTypes";
14
+
15
+ /**
16
+ * Low-level primitive shared by the eager base build and the lazy per-route
17
+ * builds. Takes a flat entrypoints list, runs `Bun.build`, and extracts a
18
+ * `ClientManifest` / `SsrManifest` covering only those entries.
19
+ */
20
+ export class ClientEntriesBundler {
21
+ #app: BundleClientEntriesInternalOptions["app"];
22
+ #entries: string[];
23
+ #plugins: BunPlugin[];
24
+ #external: readonly string[];
25
+ #externalSubpaths: readonly string[];
26
+ #externalAliases: Partial<Record<string, string>>;
27
+ #command: "build" | "start";
28
+ #outputSubdir: string;
29
+ #reactFastRefresh: boolean;
30
+ #artifactDir: string;
31
+ #outdir: string;
32
+ #servePrefix: string;
33
+ #manifest: ClientManifest = {};
34
+ #ssrManifest: SsrManifest = { moduleLoading: null, moduleMap: {} };
35
+ #entryUrlsByAbsPath = new Map<string, string>();
36
+ #entryOutputAbsByAbsPath = new Map<string, string>();
37
+ #entryDepsByAbsPath = new Map<string, string[]>();
38
+ #clientReferenceIdByAbsPath = new Map<string, string>();
39
+ #opaqueEntries: OpaqueEntryAliases | null = null;
40
+ #metafileOutputsByAbs = new Map<string, MetafileOutput>();
41
+
42
+ constructor(options: BundleClientEntriesInternalOptions) {
43
+ this.#app = options.app;
44
+ this.#entries = options.entries;
45
+ this.#plugins = options.plugins ?? [];
46
+ this.#external = options.external ?? [];
47
+ this.#externalSubpaths = options.externalSubpaths ?? [];
48
+ this.#externalAliases = options.externalAliases ?? {};
49
+ this.#command = options.command ?? "start";
50
+ this.#outputSubdir = options.outputSubdir ?? "client";
51
+ this.#reactFastRefresh = options.reactFastRefresh ?? false;
52
+ this.#artifactDir = `${this.#command === "build" ? this.#app.dist.cwdPath : this.#app.cwdPath}/.akan/artifact`;
53
+ this.#outdir = `${this.#artifactDir}/${this.#outputSubdir}`;
54
+ this.#servePrefix = `/_akan/${this.#outputSubdir}`;
55
+ }
56
+
57
+ async bundle(): Promise<BundleClientEntriesResult> {
58
+ const akanConfig = await this.#app.getConfig();
59
+ this.#opaqueEntries = await this.#createOpaqueEntryAliases();
60
+ const result = await Bun.build({
61
+ entrypoints: this.#opaqueEntries.entries,
62
+ outdir: this.#outdir,
63
+ splitting: true,
64
+ target: "browser",
65
+ format: "esm",
66
+ naming: CLIENT_BUNDLE_NAMING,
67
+ metafile: true,
68
+ define: this.#getDefine(),
69
+ minify: this.#command === "build",
70
+ optimizeImports: akanConfig.optimizeImports,
71
+ reactFastRefresh: this.#command === "start" && this.#outputSubdir === "client" && this.#reactFastRefresh,
72
+ plugins:
73
+ this.#external.length > 0 || this.#externalSubpaths.length > 0 || Object.keys(this.#externalAliases).length > 0
74
+ ? [...this.#plugins, this.#createExternalSpecifiersPlugin()]
75
+ : this.#plugins,
76
+ });
77
+ if (!result.success) throw new AggregateError(result.logs, "[ClientEntriesBundler] Bun.build failed");
78
+ await this.#rewriteExternalImportSpecifiers(result.outputs);
79
+
80
+ const metafile = result.metafile;
81
+ if (!metafile) throw new Error("[ClientEntriesBundler] metafile is missing");
82
+
83
+ this.#metafileOutputsByAbs = new Map<string, MetafileOutput>(
84
+ Object.entries(metafile.outputs).map(([k, v]) => [this.#absFromOutdir(k), v as MetafileOutput]),
85
+ );
86
+
87
+ for (const artifact of result.outputs.filter((a) => a.kind === "entry-point"))
88
+ this.#addEntryArtifact(artifact.path);
89
+
90
+ return {
91
+ manifest: this.#manifest,
92
+ ssrManifest: this.#ssrManifest,
93
+ entryUrlsByAbsPath: this.#entryUrlsByAbsPath,
94
+ entryOutputAbsByAbsPath: this.#entryOutputAbsByAbsPath,
95
+ entryDepsByAbsPath: this.#entryDepsByAbsPath,
96
+ clientReferenceIdByAbsPath: this.#clientReferenceIdByAbsPath,
97
+ };
98
+ }
99
+
100
+ #getDefine(): Record<string, string> {
101
+ const nodeEnv = this.#command === "build" ? "production" : (process.env.NODE_ENV ?? "development");
102
+ return {
103
+ "process.env.NODE_ENV": JSON.stringify(nodeEnv),
104
+ "process.env.AKAN_PUBLIC_RENDER_ENV": JSON.stringify("ssr"),
105
+ ...Object.fromEntries(
106
+ Object.entries(this.#app.getPublicEnv()).map(([key, value]) => [`process.env.${key}`, JSON.stringify(value)]),
107
+ ),
108
+ };
109
+ }
110
+
111
+ async #createOpaqueEntryAliases(): Promise<OpaqueEntryAliases> {
112
+ const aliasDir = path.join(this.#app.cwdPath, ".akan", "generated", "client-entry-alias", this.#outputSubdir);
113
+ fs.mkdirSync(aliasDir, { recursive: true });
114
+ const originalByAlias = new Map<string, string>();
115
+ const aliasedEntries = await Promise.all(
116
+ this.#entries.map(async (entry) => {
117
+ const absEntry = path.resolve(entry);
118
+ const hash = Bun.hash(`${this.#app.name}\n${this.#outputSubdir}\n${absEntry}`).toString(36);
119
+ const aliasPath = path.join(aliasDir, `${hash}.tsx`);
120
+ await Bun.write(
121
+ aliasPath,
122
+ this.#createOpaqueEntryAliasSource(absEntry, await this.#scanEntryExportNames(absEntry)),
123
+ );
124
+ originalByAlias.set(path.resolve(aliasPath), absEntry);
125
+ return aliasPath;
126
+ }),
127
+ );
128
+ return { entries: aliasedEntries, originalByAlias, aliasDir };
129
+ }
130
+
131
+ #createOpaqueEntryAliasSource(absEntry: string, exportNames: string[]): string {
132
+ const entryLit = JSON.stringify(path.resolve(absEntry));
133
+ if (exportNames.length === 0) return `export * from ${entryLit};\n`;
134
+ const namedExports = exportNames.filter((name) => name !== "default");
135
+ const lines = namedExports.length > 0 ? [`export { ${namedExports.join(", ")} } from ${entryLit};`] : [];
136
+ if (exportNames.includes("default")) lines.push(`export { default } from ${entryLit};`);
137
+ return `${lines.join("\n")}\n`;
138
+ }
139
+
140
+ async #scanEntryExportNames(absEntry: string): Promise<string[]> {
141
+ const source = await Bun.file(absEntry).text();
142
+ const transpiler = new Bun.Transpiler({ loader: this.#loaderForEntry(absEntry) });
143
+ return transpiler.scan(source).exports;
144
+ }
145
+
146
+ #loaderForEntry(absPath: string): "ts" | "tsx" | "js" | "jsx" {
147
+ if (absPath.endsWith(".tsx")) return "tsx";
148
+ if (absPath.endsWith(".jsx")) return "jsx";
149
+ if (absPath.endsWith(".ts")) return "ts";
150
+ return "js";
151
+ }
152
+
153
+ /**
154
+ * Build a BunPlugin that marks a fixed set of bare specifiers as external via `onResolve`,
155
+ * as opposed to `Bun.build({ external })`, so macro-time imports still resolve normally.
156
+ */
157
+ #createExternalSpecifiersPlugin(): BunPlugin {
158
+ const set = new Set(this.#external);
159
+ const subpathSet = new Set(this.#externalSubpaths);
160
+ const aliases = this.#externalAliases;
161
+ const specifiers = [...new Set([...this.#external, ...this.#externalSubpaths, ...Object.keys(aliases)])];
162
+ const escaped = specifiers.map((s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
163
+ const filter = new RegExp(`^(${escaped.join("|")})(?:/.*)?$`);
164
+ return {
165
+ name: "akan-externalize-specifiers",
166
+ setup(build) {
167
+ build.onResolve({ filter }, (args) => {
168
+ const alias = aliases[args.path];
169
+ if (alias) return { path: alias, external: true };
170
+ if (!ClientEntriesBundler.#matchesExternalSpecifier(args.path, set, subpathSet)) return undefined;
171
+ return { path: args.path, external: true };
172
+ });
173
+ },
174
+ };
175
+ }
176
+
177
+ static #matchesExternalSpecifier(
178
+ specifier: string,
179
+ exactExternals: Set<string>,
180
+ subpathExternals: Set<string>,
181
+ ): boolean {
182
+ if (exactExternals.has(specifier)) return true;
183
+ if (subpathExternals.has(specifier)) return true;
184
+ for (const external of subpathExternals) {
185
+ if (specifier.startsWith(`${external}/`)) return true;
186
+ }
187
+ return false;
188
+ }
189
+
190
+ async #rewriteExternalImportSpecifiers(outputs: Bun.BuildOutput["outputs"]): Promise<void> {
191
+ if (Object.keys(this.#externalAliases).length === 0) return;
192
+ await Promise.all(
193
+ outputs
194
+ .filter((output) => output.path.endsWith(".js"))
195
+ .map(async (output) => {
196
+ const source = await Bun.file(output.path).text();
197
+ const rewritten = ClientEntriesBundler.rewriteExternalImportSpecifiers(source, this.#externalAliases);
198
+ if (rewritten !== source) await Bun.write(output.path, rewritten);
199
+ }),
200
+ );
201
+ }
202
+
203
+ static rewriteExternalImportSpecifiers(source: string, aliases: Partial<Record<string, string>>): string {
204
+ let rewritten = source;
205
+ for (const [specifier, alias] of Object.entries(aliases)) {
206
+ if (!alias) continue;
207
+ const escaped = specifier.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
208
+ rewritten = rewritten
209
+ .replace(new RegExp(`(\\bfrom\\s*["'])${escaped}(["'])`, "g"), (_match, prefix, suffix) => {
210
+ return `${prefix}${alias}${suffix}`;
211
+ })
212
+ .replace(new RegExp(`(\\bimport\\s*["'])${escaped}(["'])`, "g"), (_match, prefix, suffix) => {
213
+ return `${prefix}${alias}${suffix}`;
214
+ });
215
+ }
216
+ return rewritten;
217
+ }
218
+
219
+ #toServeUrl(absOutPath: string): string {
220
+ const rel = path.relative(this.#outdir, absOutPath).split(path.sep).join("/");
221
+ return `${this.#servePrefix}/${rel}`;
222
+ }
223
+
224
+ // Bun's metafile paths are relative to `outdir`, while entry points are relative to build cwd.
225
+ #absFromOutdir(p: string): string {
226
+ return path.isAbsolute(p) ? p : path.resolve(this.#outdir, p);
227
+ }
228
+
229
+ #absFromEntryPoint(p: string): string {
230
+ if (path.isAbsolute(p)) return path.resolve(p);
231
+ const candidates = [path.resolve(process.cwd(), p), path.resolve(this.#app.cwdPath, p)];
232
+ return candidates.find((candidate) => fs.existsSync(candidate)) ?? candidates[0];
233
+ }
234
+
235
+ // Walk only static chunk imports. Dynamic imports stay lazy and outside eager manifest chunks.
236
+ #collectChunkUrls(absOutPath: string, visited = new Set<string>()): string[] {
237
+ if (visited.has(absOutPath)) return [];
238
+ visited.add(absOutPath);
239
+ const info = this.#metafileOutputsByAbs.get(absOutPath);
240
+ if (!info) return [this.#toServeUrl(absOutPath)];
241
+ const urls: string[] = [this.#toServeUrl(absOutPath)];
242
+ for (const imp of info.imports) {
243
+ if (imp.kind === "dynamic-import") continue;
244
+ const absImp = this.#absFromOutdir(imp.path);
245
+ if (!absImp.endsWith(".js")) continue;
246
+ urls.push(...this.#collectChunkUrls(absImp, visited));
247
+ }
248
+ return urls;
249
+ }
250
+
251
+ #collectClientDeps(absOutPath: string, visited = new Set<string>()): Set<string> {
252
+ if (visited.has(absOutPath)) return new Set();
253
+ visited.add(absOutPath);
254
+ const deps = new Set<string>();
255
+ const info = this.#metafileOutputsByAbs.get(absOutPath);
256
+ if (!info) return deps;
257
+ if (!this.#opaqueEntries) throw new Error("[ClientEntriesBundler] opaque entries are missing");
258
+
259
+ for (const input of Object.keys(info.inputs ?? {})) {
260
+ const absInput = this.#absFromEntryPoint(input);
261
+ if (!absInput.includes("node_modules") && !absInput.startsWith(this.#opaqueEntries.aliasDir)) deps.add(absInput);
262
+ }
263
+
264
+ for (const imp of info.imports) {
265
+ if (imp.kind === "dynamic-import") continue;
266
+ const absImp = this.#absFromOutdir(imp.path);
267
+ if (!absImp.endsWith(".js")) continue;
268
+ for (const dep of this.#collectClientDeps(absImp, visited)) deps.add(dep);
269
+ }
270
+ return deps;
271
+ }
272
+
273
+ #addEntryArtifact(absOut: string): void {
274
+ if (!this.#opaqueEntries) throw new Error("[ClientEntriesBundler] opaque entries are missing");
275
+ const info = this.#metafileOutputsByAbs.get(absOut);
276
+ const buildEntryPointAbs = info?.entryPoint ? this.#absFromEntryPoint(info.entryPoint) : null;
277
+ const entryPointAbs = buildEntryPointAbs
278
+ ? (this.#opaqueEntries.originalByAlias.get(buildEntryPointAbs) ?? buildEntryPointAbs)
279
+ : null;
280
+ if (!entryPointAbs) return;
281
+
282
+ const chunkUrls = this.#collectChunkUrls(absOut);
283
+ const entryUrl = chunkUrls[0] ?? this.#toServeUrl(absOut);
284
+ this.#entryUrlsByAbsPath.set(entryPointAbs, entryUrl);
285
+ this.#entryOutputAbsByAbsPath.set(entryPointAbs, absOut);
286
+ this.#entryDepsByAbsPath.set(entryPointAbs, [...this.#collectClientDeps(absOut)].sort());
287
+ const clientReferenceId = toClientReferencePath(entryPointAbs, this.#app.workspace.workspaceRoot);
288
+ this.#clientReferenceIdByAbsPath.set(entryPointAbs, clientReferenceId);
289
+
290
+ // react-server-dom-webpack expects flat [chunkId, chunkUrl, ...] pairs.
291
+ const flatChunks: string[] = [];
292
+ for (const url of chunkUrls) flatChunks.push(url, url);
293
+
294
+ const exportNames = info?.exports && info.exports.length > 0 ? info.exports : ["default"];
295
+ const ssrEntriesByName: Record<string, SsrManifestEntry> = {};
296
+ for (const name of exportNames) {
297
+ const key = `${clientReferenceId}#${name}`;
298
+ this.#manifest[key] = { id: entryUrl, chunks: flatChunks, name, async: true };
299
+ ssrEntriesByName[name] = { id: absOut, chunks: [absOut, absOut], name, async: true };
300
+ }
301
+ this.#ssrManifest.moduleMap[entryUrl] = ssrEntriesByName;
302
+ }
303
+ }
@@ -0,0 +1,199 @@
1
+ import path from "node:path";
2
+ import type { App } from "../commandDecorators";
3
+ import { BarrelAnalyzer } from "../transforms/barrelAnalyzer";
4
+ import { createTsconfigPackageResolver, rewriteBarrelImports } from "../transforms/barrelImportsPlugin";
5
+ import type { AkanConfig, ClientEntryDiscovery, ScannedImport } from "./clientBuildTypes";
6
+
7
+ const USE_CLIENT_RE = /^\s*(?:\/\*[\s\S]*?\*\/\s*|\/\/[^\n]*\n\s*)*["']use client["']/;
8
+ const SOURCE_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
9
+ const NODE_MODULES_RE = /[\\/]node_modules[\\/]/;
10
+ const AKANJS_NODE_MODULE_RE = /[\\/]node_modules[\\/]akanjs[\\/]/;
11
+ // File extensions whose imports can't produce React client components and that
12
+ // the bundler handles via dedicated loaders. Skipping them avoids resolver
13
+ // noise (Bun's scanImports surfaces CSS / asset imports too).
14
+ const NON_SOURCE_EXT_RE = /\.(css|scss|sass|less|json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav)$/i;
15
+ type PackageResolver = Awaited<ReturnType<typeof createTsconfigPackageResolver>>;
16
+
17
+ const shouldSkipNodeModule = (absPath: string) => NODE_MODULES_RE.test(absPath) && !AKANJS_NODE_MODULE_RE.test(absPath);
18
+
19
+ /**
20
+ * Graph-based `"use client"` discovery, seeded from an explicit file list.
21
+ *
22
+ * Walks imports — including dynamic `import()` — while flattening barrel
23
+ * specifiers through the same `BarrelAnalyzer` the runtime plugin uses, so
24
+ * the traversal matches the module graph the bundler will actually see.
25
+ */
26
+ export class GraphClientEntryDiscovery implements ClientEntryDiscovery {
27
+ #akanConfig: AkanConfig;
28
+ #resolvePackage: PackageResolver;
29
+ #analyzer: BarrelAnalyzer;
30
+ #tsTranspiler = new Bun.Transpiler({ loader: "tsx" });
31
+ #fileExistsCache = new Map<string, Promise<boolean>>();
32
+ #readCache = new Map<string, Promise<string | null>>();
33
+ #rewriteCache = new Map<string, Promise<string>>();
34
+ #importCache = new Map<string, Promise<ScannedImport[]>>();
35
+ #resolvedFileCache = new Map<string, Promise<string | null>>();
36
+ #resolvedSpecifierCache = new Map<string, Promise<string | null>>();
37
+ #reachableEntriesCache = new Map<string, Set<string>>();
38
+
39
+ constructor(akanConfig: AkanConfig, resolvePackage: PackageResolver) {
40
+ this.#akanConfig = akanConfig;
41
+ this.#resolvePackage = resolvePackage;
42
+ this.#analyzer = new BarrelAnalyzer({ resolvePackage });
43
+ }
44
+
45
+ static async create(app: App): Promise<GraphClientEntryDiscovery> {
46
+ return new GraphClientEntryDiscovery(await app.getConfig(), await createTsconfigPackageResolver(app));
47
+ }
48
+
49
+ async discover(seeds: string[]): Promise<string[]> {
50
+ const entries = new Set<string>();
51
+ for (const seed of seeds) {
52
+ for (const entry of await this.#discoverFromFile(seed, new Set())) entries.add(entry);
53
+ }
54
+ return Array.from(entries).sort();
55
+ }
56
+
57
+ invalidate(files: string[]): void {
58
+ for (const file of files) {
59
+ const absPath = path.resolve(file);
60
+ this.#readCache.delete(absPath);
61
+ this.#rewriteCache.delete(absPath);
62
+ this.#importCache.delete(absPath);
63
+ this.#reachableEntriesCache.delete(absPath);
64
+ }
65
+ // Parent files cache the transitive result of their imports, so a changed
66
+ // child can affect any reachable-entry cache above it.
67
+ if (files.length > 0) this.#reachableEntriesCache.clear();
68
+ }
69
+
70
+ async #fileExists(p: string): Promise<boolean> {
71
+ const absPath = path.resolve(p);
72
+ let cached = this.#fileExistsCache.get(absPath);
73
+ if (!cached) {
74
+ cached = Bun.file(absPath).exists();
75
+ this.#fileExistsCache.set(absPath, cached);
76
+ }
77
+ return cached;
78
+ }
79
+
80
+ #readFile(file: string): Promise<string | null> {
81
+ const absPath = path.resolve(file);
82
+ let cached = this.#readCache.get(absPath);
83
+ if (!cached) {
84
+ cached = Bun.file(absPath)
85
+ .text()
86
+ .catch(() => null);
87
+ this.#readCache.set(absPath, cached);
88
+ }
89
+ return cached;
90
+ }
91
+
92
+ async #resolveFileCandidate(absPathNoExt: string): Promise<string | null> {
93
+ const cacheKey = path.resolve(absPathNoExt);
94
+ let cached = this.#resolvedFileCache.get(cacheKey);
95
+ if (cached) return cached;
96
+ cached = (async () => {
97
+ if (await this.#fileExists(cacheKey)) return cacheKey;
98
+ for (const ext of SOURCE_EXTS) {
99
+ const f = `${cacheKey}${ext}`;
100
+ if (await this.#fileExists(f)) return f;
101
+ }
102
+ for (const ext of SOURCE_EXTS) {
103
+ const f = path.join(cacheKey, `index${ext}`);
104
+ if (await this.#fileExists(f)) return f;
105
+ }
106
+ return null;
107
+ })();
108
+ this.#resolvedFileCache.set(cacheKey, cached);
109
+ return cached;
110
+ }
111
+
112
+ async #resolveSpecifier(spec: string, importerDir: string): Promise<string | null> {
113
+ const cacheKey = `${importerDir}\0${spec}`;
114
+ let cached = this.#resolvedSpecifierCache.get(cacheKey);
115
+ if (cached) return cached;
116
+ cached = (async () => {
117
+ if (spec.startsWith(".") || spec.startsWith("/")) {
118
+ const abs = spec.startsWith("/") ? spec : path.resolve(importerDir, spec);
119
+ return this.#resolveFileCandidate(abs);
120
+ }
121
+ const pkg = await this.#resolvePackage(spec);
122
+ if (pkg) return pkg.entryFile;
123
+ return null;
124
+ })();
125
+ this.#resolvedSpecifierCache.set(cacheKey, cached);
126
+ return cached;
127
+ }
128
+
129
+ async #getRewrittenSource(file: string, content: string): Promise<string> {
130
+ const absPath = path.resolve(file);
131
+ let cached = this.#rewriteCache.get(absPath);
132
+ if (!cached) {
133
+ cached = (async () => {
134
+ if (this.#akanConfig.barrelImports.length === 0) return content;
135
+ try {
136
+ return (await rewriteBarrelImports(content, this.#akanConfig.barrelImports, this.#analyzer)) ?? content;
137
+ } catch {
138
+ return content;
139
+ }
140
+ })();
141
+ this.#rewriteCache.set(absPath, cached);
142
+ }
143
+ return cached;
144
+ }
145
+
146
+ async #getImports(file: string, source: string): Promise<ScannedImport[]> {
147
+ const absPath = path.resolve(file);
148
+ let cached = this.#importCache.get(absPath);
149
+ if (!cached) {
150
+ cached = Promise.resolve().then(() => {
151
+ try {
152
+ return this.#tsTranspiler.scanImports(source);
153
+ } catch {
154
+ return [];
155
+ }
156
+ });
157
+ this.#importCache.set(absPath, cached);
158
+ }
159
+ return cached;
160
+ }
161
+
162
+ async #discoverFromFile(file: string, visiting: Set<string>): Promise<Set<string>> {
163
+ const absPath = path.resolve(file);
164
+ const cached = this.#reachableEntriesCache.get(absPath);
165
+ if (cached) return new Set(cached);
166
+ if (visiting.has(absPath) || shouldSkipNodeModule(absPath)) return new Set();
167
+
168
+ visiting.add(absPath);
169
+ const entries = new Set<string>();
170
+ const content = await this.#readFile(absPath);
171
+ if (content === null) return this.#finishDiscovery(absPath, visiting, entries);
172
+
173
+ if (USE_CLIENT_RE.test(content)) {
174
+ entries.add(absPath);
175
+ return this.#finishDiscovery(absPath, visiting, entries);
176
+ }
177
+
178
+ const source = await this.#getRewrittenSource(absPath, content);
179
+ const imports = await this.#getImports(absPath, source);
180
+ const importerDir = path.dirname(absPath);
181
+ for (const imp of imports) {
182
+ const spec = imp.path;
183
+ if (!spec) continue;
184
+ if (NON_SOURCE_EXT_RE.test(spec)) continue;
185
+ const resolved = await this.#resolveSpecifier(spec, importerDir);
186
+ if (!resolved) continue;
187
+ if (shouldSkipNodeModule(resolved)) continue;
188
+ for (const entry of await this.#discoverFromFile(resolved, visiting)) entries.add(entry);
189
+ }
190
+
191
+ return this.#finishDiscovery(absPath, visiting, entries);
192
+ }
193
+
194
+ #finishDiscovery(absPath: string, visiting: Set<string>, entries: Set<string>): Set<string> {
195
+ visiting.delete(absPath);
196
+ this.#reachableEntriesCache.set(absPath, entries);
197
+ return new Set(entries);
198
+ }
199
+ }