@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,51 @@
1
+ import { afterEach, describe, expect, mock, test } from "bun:test";
2
+ import { IncrementalBuilderHost } from "./incrementalBuilder.host";
3
+
4
+ const originalSpawn = Bun.spawn;
5
+
6
+ const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
7
+
8
+ afterEach(() => {
9
+ (Bun as unknown as { spawn: typeof Bun.spawn }).spawn = originalSpawn;
10
+ mock.restore();
11
+ });
12
+
13
+ describe("IncrementalBuilderHost", () => {
14
+ test("restarts after a ready builder exits", async () => {
15
+ const spawns: Array<{
16
+ proc: { pid: number; send: ReturnType<typeof mock>; kill: ReturnType<typeof mock>; killed: boolean };
17
+ options: { ipc?: (message: unknown) => void; onExit?: () => void };
18
+ }> = [];
19
+ (Bun as unknown as { spawn: typeof Bun.spawn }).spawn = mock((_, options) => {
20
+ const proc = { pid: 10_000 + spawns.length, send: mock(), kill: mock(), killed: false };
21
+ spawns.push({ proc, options: options as { ipc?: (message: unknown) => void; onExit?: () => void } });
22
+ return proc as never;
23
+ }) as never;
24
+
25
+ const onReady = mock();
26
+ const onRestartReady = mock();
27
+ const host = new IncrementalBuilderHost({
28
+ app: { cwdPath: "/tmp/app" } as never,
29
+ entry: "/tmp/builder.ts",
30
+ env: {},
31
+ onMessage: () => undefined,
32
+ });
33
+
34
+ host.start({ onReady, onRestartReady });
35
+ spawns[0]?.options.ipc?.({ type: "builder-ready" });
36
+ expect(onReady).toHaveBeenCalledTimes(1);
37
+ expect(host.status).toBe("ready");
38
+
39
+ spawns[0]?.options.onExit?.();
40
+ expect(host.status).toBe("restarting");
41
+ expect(host.send({ type: "build-route", id: 1, routeId: "a", seeds: [], knownEntries: [] })).toBe(false);
42
+
43
+ await wait(1_050);
44
+ expect(spawns).toHaveLength(2);
45
+ spawns[1]?.options.ipc?.({ type: "builder-ready" });
46
+ expect(onRestartReady).toHaveBeenCalledTimes(1);
47
+ expect(host.status).toBe("ready");
48
+
49
+ host.stop();
50
+ });
51
+ });
@@ -0,0 +1,152 @@
1
+ import path from "node:path";
2
+ import { Logger } from "akanjs/common";
3
+ import type { BuilderMessage } from "akanjs/server";
4
+ import type { App } from "../commandDecorators";
5
+
6
+ const builderMsgTypeSet = new Set<BuilderMessage["type"]>([
7
+ "build-route-res",
8
+ "builder-ready",
9
+ "invalidate",
10
+ "css-updated",
11
+ "pages-updated",
12
+ ]);
13
+ interface IncrementalBuilderHostOptions {
14
+ app: App;
15
+ entry: string;
16
+ env: Record<string, string>;
17
+ onMessage: (message: BuilderMessage) => void;
18
+ }
19
+
20
+ type IncrementalBuilderStatus = "starting" | "ready" | "restarting" | "stopped";
21
+
22
+ interface IncrementalBuilderStartOptions {
23
+ onExit?: () => void;
24
+ onReady?: () => void;
25
+ onRestartReady?: () => void;
26
+ }
27
+
28
+ export class IncrementalBuilderHost {
29
+ static readonly #restartBaseDelayMs = 1_000;
30
+ static readonly #restartMaxDelayMs = 30_000;
31
+ logger = new Logger("IncrementalBuilderHost");
32
+ entry: string;
33
+ env: Record<string, string>;
34
+ app: App;
35
+ ready = false;
36
+ readonly #onMessage: (message: BuilderMessage) => void;
37
+ #proc: Bun.Subprocess<"ignore", "inherit", "inherit"> | null = null;
38
+ #status: IncrementalBuilderStatus = "stopped";
39
+ #restartAttempts = 0;
40
+ #restartTimer: ReturnType<typeof setTimeout> | null = null;
41
+ #manualStop = false;
42
+ #startOptions: IncrementalBuilderStartOptions = {};
43
+ constructor({ app, entry, env, onMessage }: IncrementalBuilderHostOptions) {
44
+ this.app = app;
45
+ this.entry = entry;
46
+ this.env = env;
47
+ this.#onMessage = onMessage;
48
+ }
49
+ get status() {
50
+ return this.#status;
51
+ }
52
+ start(options: IncrementalBuilderStartOptions = {}) {
53
+ if (this.#proc) this.stop();
54
+ this.#manualStop = false;
55
+ this.#startOptions = options;
56
+ this.#spawn(false);
57
+ return this;
58
+ }
59
+ #spawn(isRestart: boolean) {
60
+ this.#status = isRestart ? "restarting" : "starting";
61
+ this.ready = false;
62
+ let proc!: Bun.Subprocess<"ignore", "inherit", "inherit">;
63
+ proc = Bun.spawn(["bun", this.entry], {
64
+ cwd: this.app.cwdPath,
65
+ env: { ...this.env, AKAN_WATCH: "1" },
66
+ stdio: ["ignore", "inherit", "inherit"],
67
+ ipc: (msg: BuilderMessage) => {
68
+ if (this.#proc !== proc) return;
69
+ if (!msg || typeof msg !== "object") return;
70
+ if (builderMsgTypeSet.has(msg.type)) this.#onMessage(msg);
71
+ if (msg.type === "builder-ready" && !this.ready) {
72
+ this.ready = true;
73
+ this.#status = "ready";
74
+ this.#restartAttempts = 0;
75
+ if (isRestart) this.#startOptions.onRestartReady?.();
76
+ else this.#startOptions.onReady?.();
77
+ }
78
+ },
79
+ serialization: "advanced",
80
+ onExit: () => {
81
+ if (this.#proc !== proc) return;
82
+ this.#proc = null;
83
+ const wasReady = this.ready;
84
+ this.ready = false;
85
+ if (this.#manualStop || this.#status === "stopped") return;
86
+ if (!wasReady) {
87
+ this.#status = "stopped";
88
+ this.#startOptions.onExit?.();
89
+ return;
90
+ }
91
+ this.#scheduleRestart();
92
+ },
93
+ });
94
+ this.#proc = proc;
95
+ this.logger.verbose(`builder spawned pid=${proc.pid} entry=${this.entry}${isRestart ? " restart=1" : ""}`);
96
+ }
97
+ #scheduleRestart() {
98
+ if (this.#manualStop || this.#restartTimer) return;
99
+ this.#status = "restarting";
100
+ const attempt = this.#restartAttempts;
101
+ const delay = Math.min(
102
+ IncrementalBuilderHost.#restartBaseDelayMs * 2 ** attempt,
103
+ IncrementalBuilderHost.#restartMaxDelayMs,
104
+ );
105
+ this.#restartAttempts = attempt + 1;
106
+ this.logger.warn(`builder exited after ready; restarting in ${delay}ms (attempt ${this.#restartAttempts})`);
107
+ this.#restartTimer = setTimeout(() => {
108
+ this.#restartTimer = null;
109
+ if (this.#manualStop) return;
110
+ this.#spawn(true);
111
+ }, delay);
112
+ }
113
+ send(message: BuilderMessage): boolean {
114
+ if (!this.#proc || this.#status !== "ready") {
115
+ this.logger.warn(`incrementalBuilderHost is ${this.#status}; cannot send ${message.type}`);
116
+ return false;
117
+ }
118
+ try {
119
+ this.#proc.send(message);
120
+ return true;
121
+ } catch (error) {
122
+ this.logger.warn(
123
+ `failed to send ${message.type} to builder: ${error instanceof Error ? error.message : String(error)}`,
124
+ );
125
+ return false;
126
+ }
127
+ }
128
+ stop() {
129
+ this.#manualStop = true;
130
+ if (this.#restartTimer) {
131
+ clearTimeout(this.#restartTimer);
132
+ this.#restartTimer = null;
133
+ }
134
+ if (this.#proc) this.#proc.kill();
135
+ this.#proc = null;
136
+ this.ready = false;
137
+ this.#status = "stopped";
138
+ }
139
+ static async create(app: App, env: Record<string, string>, onMessage: (message: BuilderMessage) => void) {
140
+ const candidates = [
141
+ path.join(app.workspace.workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts"),
142
+ path.join(
143
+ app.workspace.workspaceRoot,
144
+ "node_modules/@akanjs/devkit/incrementalBuilder/incrementalBuilder.proc.ts",
145
+ ),
146
+ path.join(import.meta.dir, "incrementalBuilder.proc.ts"),
147
+ ];
148
+ for (const c of candidates)
149
+ if (await Bun.file(c).exists()) return new IncrementalBuilderHost({ app, entry: c, env, onMessage });
150
+ throw new Error(`[cli] frontend builder entry not found; looked in: ${candidates.join(", ")}`);
151
+ }
152
+ }
@@ -0,0 +1,331 @@
1
+ import path from "node:path";
2
+ import { Logger } from "akanjs/common";
3
+ import {
4
+ type App,
5
+ AppExecutor,
6
+ type ChangeBatch,
7
+ type ClientEntryDiscovery,
8
+ CsrArtifactBuilder,
9
+ type CssCompiler,
10
+ FontOptimizer,
11
+ GraphClientEntryDiscovery,
12
+ HmrWatcher,
13
+ PagesBundleBuilder,
14
+ RouteClientBuilder,
15
+ SsrBaseArtifactBuilder,
16
+ WatchRootResolver,
17
+ WorkspaceExecutor,
18
+ } from "@akanjs/devkit";
19
+ import type {
20
+ BaseBuildArtifact,
21
+ BuilderEvent,
22
+ BuilderMessage,
23
+ BuilderReq,
24
+ BuilderRes,
25
+ BuildRouteResultPayload,
26
+ } from "akanjs/server";
27
+
28
+ interface IncrementalBuilderOptions {
29
+ app: App;
30
+ artifact: BaseBuildArtifact;
31
+ watch: boolean;
32
+ cssCompiler: CssCompiler;
33
+ optimizedFonts: Awaited<ReturnType<FontOptimizer["optimize"]>>;
34
+ discovery: ClientEntryDiscovery;
35
+ }
36
+
37
+ class IncrementalBuilder {
38
+ #logger = new Logger("IncrementalBuilder");
39
+ #app: App;
40
+ #artifact: BaseBuildArtifact;
41
+ #watch: boolean;
42
+ #cssCompiler: CssCompiler;
43
+ #optimizedFonts: Awaited<ReturnType<FontOptimizer["optimize"]>>;
44
+ #discovery: ClientEntryDiscovery;
45
+ #generation = 0;
46
+ #workQueue: Promise<void> = Promise.resolve();
47
+ #cssRebuildQueue: Promise<void> = Promise.resolve();
48
+ #cssRebuildTimer: ReturnType<typeof setTimeout> | null = null;
49
+ #pendingCssRebuild: { artifactDir: string; refresh: boolean; generation?: number; changedFiles?: string[] } | null =
50
+ null;
51
+ constructor(options: IncrementalBuilderOptions) {
52
+ this.#app = options.app;
53
+ this.#artifact = options.artifact;
54
+ this.#watch = options.watch;
55
+ this.#cssCompiler = options.cssCompiler;
56
+ this.#optimizedFonts = options.optimizedFonts;
57
+ this.#discovery = options.discovery;
58
+ }
59
+
60
+ async handleBuildRoute(msg: BuilderReq): Promise<BuilderRes> {
61
+ return this.#enqueueWork(`build-route:${msg.routeId}`, async () => this.#handleBuildRoute(msg));
62
+ }
63
+
64
+ async #handleBuildRoute(msg: BuilderReq): Promise<BuilderRes> {
65
+ try {
66
+ const delta = await new RouteClientBuilder({
67
+ app: this.#app,
68
+ routeId: msg.routeId,
69
+ seeds: msg.seeds,
70
+ artifact: this.#artifact,
71
+ knownEntries: new Set<string>(msg.knownEntries),
72
+ discovery: this.#discovery,
73
+ }).build();
74
+ this.#logger.verbose(`build-route ok routeId=${msg.routeId} newEntries=${delta.newEntries.length}`);
75
+ return {
76
+ type: "build-route-res",
77
+ id: msg.id,
78
+ ok: true,
79
+ data: {
80
+ manifestDelta: delta.manifestDelta as BuildRouteResultPayload["manifestDelta"],
81
+ ssrManifestDelta: delta.ssrManifestDelta.moduleMap as BuildRouteResultPayload["ssrManifestDelta"],
82
+ newEntries: delta.newEntries,
83
+ discoveredEntries: delta.discoveredEntries,
84
+ clientDeps: delta.clientDeps,
85
+ clientDepsByEntry: delta.clientDepsByEntry,
86
+ routeId: msg.routeId,
87
+ generation: msg.generation,
88
+ } as BuildRouteResultPayload,
89
+ };
90
+ } catch (err) {
91
+ const errMsg = err instanceof Error ? err.message : String(err);
92
+ this.#logger.error(`build-route failed routeId=${msg.routeId}: ${errMsg}`);
93
+ return { type: "build-route-res", id: msg.id, ok: false, error: errMsg };
94
+ }
95
+ }
96
+ async #enqueueWork<T>(label: string, fn: () => Promise<T>): Promise<T> {
97
+ const started = Date.now();
98
+ const run = this.#workQueue.then(fn, fn);
99
+ this.#workQueue = run.then(() => undefined).catch(() => undefined);
100
+ try {
101
+ return await run;
102
+ } finally {
103
+ this.#logger.verbose(`[work-queue] ${label} finished in ${Date.now() - started}ms`);
104
+ }
105
+ }
106
+ batchTouchesPagesTree(appDir: string, batch: ChangeBatch): boolean {
107
+ const absAppDir = path.resolve(appDir);
108
+ for (const f of batch.files) {
109
+ const abs = path.resolve(f);
110
+ if (!abs.startsWith(`${absAppDir}${path.sep}`) && abs !== absAppDir) continue;
111
+ if (/\.(tsx|ts|jsx|js)$/.test(abs)) return true;
112
+ }
113
+ return false;
114
+ }
115
+ async batchMayChangePageKeys(appDir: string, batch: ChangeBatch): Promise<boolean> {
116
+ const absAppDir = path.resolve(appDir);
117
+ const pageKeys = new Set((await this.#app.getPageKeys()).map((key) => path.normalize(key)));
118
+ for (const f of batch.files) {
119
+ const abs = path.resolve(f);
120
+ if (!abs.startsWith(`${absAppDir}${path.sep}`) && abs !== absAppDir) continue;
121
+ if (!/\.(tsx|ts|jsx|js)$/.test(abs)) continue;
122
+ const rel = path.normalize(path.relative(absAppDir, abs));
123
+ if (!(await Bun.file(abs).exists()) || !pageKeys.has(rel)) return true;
124
+ }
125
+ return false;
126
+ }
127
+ async rebuildCssArtifact(
128
+ artifactDir: string,
129
+ { refresh, generation, changedFiles }: { refresh: boolean; generation?: number; changedFiles?: string[] },
130
+ ) {
131
+ const cssStarted = Date.now();
132
+ const cssByBasePathStarted = Date.now();
133
+ const cssByBasePath = await this.#cssCompiler.getCssByBasePath({ refresh });
134
+ this.#logger.verbose(`css-get-by-base-path ok (${Date.now() - cssByBasePathStarted}ms)`);
135
+ const fontStarted = Date.now();
136
+ const optimizedFonts = await this.#getOptimizedFonts(changedFiles ?? []);
137
+ this.#logger.verbose(`font-assets ready (${Date.now() - fontStarted}ms)`);
138
+ const cssAssetEntries: Array<[string, { cssUrl: string; cssRelPath: string }]> = [];
139
+ const cssBase64ByUrl: Record<string, string> = {};
140
+ await Promise.all(
141
+ Object.entries(cssByBasePath).flatMap(([basePath, baseCssText]) => {
142
+ const cssText = [baseCssText, optimizedFonts.css].filter(Boolean).join("\n");
143
+ if (!cssText) return [];
144
+ return [
145
+ (async () => {
146
+ const cssAssetName = basePath || "root";
147
+ const cssHash = Bun.hash(`${basePath}\n${cssText}`).toString(36);
148
+ const cssRelPath = `styles/${cssAssetName}-${cssHash}.css`;
149
+ const cssUrl = `/_akan/styles/${cssAssetName}-${cssHash}.css`;
150
+ await Bun.write(path.join(artifactDir, cssRelPath), cssText);
151
+ cssAssetEntries.push([basePath, { cssUrl, cssRelPath }]);
152
+ cssBase64ByUrl[cssUrl] = Buffer.from(new TextEncoder().encode(cssText)).toString("base64");
153
+ })(),
154
+ ];
155
+ }),
156
+ );
157
+ const cssAssets = Object.fromEntries(cssAssetEntries);
158
+ if (JSON.stringify(this.#artifact.cssAssets ?? {}) === JSON.stringify(cssAssets)) {
159
+ this.#logger.verbose("css-rebuild unchanged assets; broadcast skipped");
160
+ return;
161
+ }
162
+ this.#artifact = { ...this.#artifact, cssAssets };
163
+ this.#logger.verbose(`css-compile ok assets=${Object.keys(cssAssets).length} (${Date.now() - cssStarted}ms)`);
164
+ process.send?.({
165
+ type: "css-updated",
166
+ data: {
167
+ cssAssets,
168
+ cssBase64ByUrl,
169
+ generation,
170
+ changedFiles,
171
+ },
172
+ });
173
+ }
174
+
175
+ scheduleCssRebuild(
176
+ artifactDir: string,
177
+ { refresh, generation, changedFiles }: { refresh: boolean; generation?: number; changedFiles?: string[] },
178
+ ) {
179
+ this.#pendingCssRebuild = { artifactDir, refresh, generation, changedFiles };
180
+ if (this.#cssRebuildTimer) clearTimeout(this.#cssRebuildTimer);
181
+ this.#cssRebuildTimer = setTimeout(() => {
182
+ this.#cssRebuildTimer = null;
183
+ const next = this.#pendingCssRebuild;
184
+ this.#pendingCssRebuild = null;
185
+ if (!next) return;
186
+ this.#cssRebuildQueue = this.#cssRebuildQueue
187
+ .then(async () => {
188
+ const started = Date.now();
189
+ await this.rebuildCssArtifact(next.artifactDir, {
190
+ refresh: next.refresh,
191
+ generation: next.generation,
192
+ changedFiles: next.changedFiles,
193
+ });
194
+ this.#logger.verbose(`css-rebuild checked (${Date.now() - started}ms)`);
195
+ })
196
+ .catch((err) => {
197
+ this.#logger.error(`css-rebuild failed: ${err instanceof Error ? err.message : err}`);
198
+ });
199
+ }, 150);
200
+ }
201
+
202
+ async #getOptimizedFonts(changedFiles: string[]) {
203
+ if (!this.#shouldReoptimizeFonts(changedFiles)) {
204
+ this.#logger.verbose(`font-optimize cached files=${this.#optimizedFonts.files.length}`);
205
+ return this.#optimizedFonts;
206
+ }
207
+ const started = Date.now();
208
+ this.#optimizedFonts = await new FontOptimizer(this.#app, "start").optimize();
209
+ this.#logger.verbose(`font-optimize ok files=${this.#optimizedFonts.files.length} (${Date.now() - started}ms)`);
210
+ return this.#optimizedFonts;
211
+ }
212
+
213
+ #shouldReoptimizeFonts(changedFiles: string[]) {
214
+ if (changedFiles.length === 0) return false;
215
+ return changedFiles.some((file) => {
216
+ const normalized = path.resolve(file);
217
+ if (/\.(woff2?|ttf|otf)$/i.test(normalized)) return true;
218
+ return this.#optimizedFonts.files.some((fontFile) => path.resolve(fontFile) === normalized);
219
+ });
220
+ }
221
+ async installWatcher() {
222
+ const [appDir, artifactDir] = [`${this.#app.cwdPath}/page`, `${this.#app.cwdPath}/.akan/artifact`];
223
+ const roots = await new WatchRootResolver(this.#app).resolve();
224
+ const watcher = new HmrWatcher({
225
+ roots,
226
+ logger: this.#logger,
227
+ onBatch: async (batch: ChangeBatch) => {
228
+ await this.#enqueueWork("hmr-batch", async () => this.#handleWatchBatch(appDir, artifactDir, batch));
229
+ },
230
+ });
231
+ watcher.start();
232
+ this.#logger.verbose(`watching ${roots.length} roots`);
233
+ }
234
+
235
+ async #handleWatchBatch(appDir: string, artifactDir: string, batch: ChangeBatch) {
236
+ const kinds = [...batch.kinds] as ("code" | "css" | "config")[];
237
+ if (kinds.length === 0) return;
238
+ const generation = ++this.#generation;
239
+ this.#logger.verbose(`[hmr] batch generation=${generation} kinds=${kinds.join(",")} files=${batch.files.length}`);
240
+
241
+ if (kinds.includes("code")) {
242
+ const started = Date.now();
243
+ if (kinds.includes("config")) this.#discovery = await GraphClientEntryDiscovery.create(this.#app);
244
+ else this.#discovery.invalidate?.(batch.files);
245
+ this.#logger.verbose(
246
+ `client-entry-discovery ${kinds.includes("config") ? "refreshed" : "invalidated"} (${Date.now() - started}ms)`,
247
+ );
248
+ }
249
+
250
+ if (kinds.includes("code") && (await this.batchMayChangePageKeys(appDir, batch))) {
251
+ const started = Date.now();
252
+ await this.#app.getPageKeys({ refresh: true });
253
+ this.#logger.verbose(`pageKeys updated, app pageKeys are refreshed (${Date.now() - started}ms)`);
254
+ } else if (kinds.includes("code") && this.batchTouchesPagesTree(appDir, batch)) {
255
+ this.#logger.verbose("pageKeys refresh skipped; changed page source cannot add/remove a route key");
256
+ }
257
+
258
+ if (kinds.includes("code") && this.#shouldRebuildCsr()) {
259
+ try {
260
+ const started = Date.now();
261
+ await new CsrArtifactBuilder(this.#app).build();
262
+ this.#logger.verbose(`csr-rebundle ok (${Date.now() - started}ms)`);
263
+ } catch (err) {
264
+ this.#logger.error(`csr-rebundle failed: ${err instanceof Error ? err.message : err}`);
265
+ }
266
+ } else if (kinds.includes("code")) {
267
+ this.#logger.verbose(`csr-rebundle skipped; set AKAN_DEV_CSR_REBUILD=1 to enable per-save CSR rebuilds`);
268
+ }
269
+
270
+ const event: BuilderEvent = { type: "invalidate", kinds, files: batch.files, generation };
271
+ process.send?.(event);
272
+
273
+ if (kinds.includes("code")) {
274
+ try {
275
+ const started = Date.now();
276
+ const next = await new PagesBundleBuilder(this.#app).build();
277
+ process.send?.({
278
+ type: "pages-updated",
279
+ data: { bundlePath: next.bundlePath, buildId: next.buildId, generation, changedFiles: batch.files },
280
+ });
281
+ this.#logger.verbose(`pages-rebundle ok buildId=${next.buildId} (${Date.now() - started}ms)`);
282
+ } catch (err) {
283
+ this.#logger.error(`pages-rebundle failed: ${err instanceof Error ? err.message : err}`);
284
+ }
285
+ }
286
+ if (kinds.includes("code") || kinds.includes("css")) {
287
+ this.scheduleCssRebuild(artifactDir, { refresh: true, generation, changedFiles: batch.files });
288
+ this.#logger.verbose(`css-rebuild scheduled generation=${generation}`);
289
+ }
290
+ }
291
+
292
+ async boot(): Promise<void> {
293
+ process.on("message", async (msg: BuilderMessage) => {
294
+ if (!msg || typeof msg !== "object") return;
295
+ switch (msg.type) {
296
+ case "build-route": {
297
+ const res = await this.handleBuildRoute(msg);
298
+ process.send?.(res);
299
+ return;
300
+ }
301
+ default:
302
+ return;
303
+ }
304
+ });
305
+ if (this.#watch) await this.installWatcher();
306
+ process.send?.({ type: "builder-ready" });
307
+ this.#logger.verbose(`ready (watch=${this.#watch})`);
308
+ }
309
+
310
+ #shouldRebuildCsr() {
311
+ // CSR is served by `akn start`, so rebuild dev CSR artifacts until incremental CSR HMR is implemented.
312
+ return true;
313
+ }
314
+
315
+ static async create() {
316
+ const { appName, repoName, workspaceRoot } = WorkspaceExecutor.getBaseDevEnv();
317
+ if (!workspaceRoot || !appName) throw new Error("AKAN_WORKSPACE_ROOT or AKAN_PUBLIC_APP_NAME is not set");
318
+ const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot, repoName });
319
+ const app = AppExecutor.from(workspace, appName);
320
+ const watch = process.env.AKAN_WATCH !== "0";
321
+ const { artifact, cssCompiler, optimizedFonts } = await new SsrBaseArtifactBuilder(app).build();
322
+ await new CsrArtifactBuilder(app).build();
323
+ const discovery = await GraphClientEntryDiscovery.create(app);
324
+ return new IncrementalBuilder({ app, cssCompiler, artifact, watch, optimizedFonts, discovery });
325
+ }
326
+ }
327
+
328
+ void (await IncrementalBuilder.create()).boot().catch((err) => {
329
+ console.error(err);
330
+ process.exit(1);
331
+ });
@@ -0,0 +1 @@
1
+ export * from "./incrementalBuilder.host";
@@ -1,23 +1,36 @@
1
+ export * from "./aiEditor";
2
+ export * from "./akanApp";
3
+ export * from "./akanConfig";
4
+ export * from "./applicationBuildReporter";
5
+ export * from "./applicationBuildRunner";
6
+ export * from "./applicationReleasePackager";
7
+ export * from "./artifact";
8
+ export * from "./auth";
9
+ export * from "./builder";
10
+ export * from "./capacitorApp";
11
+ export * from "./commandDecorators";
12
+ export * from "./constants";
1
13
  export * from "./createTunnel";
14
+ export * from "./dependencyScanner";
15
+ export * from "./executors";
16
+ export * from "./extractDeps";
17
+ export * from "./fileSys";
18
+ export * from "./frontendBuild";
2
19
  export * from "./getCredentials";
3
- export * from "./uploadRelease";
20
+ export * from "./getDirname";
4
21
  export * from "./getModelFileData";
5
22
  export * from "./getRelatedCnsts";
23
+ export * from "./guideline";
24
+ export * from "./incrementalBuilder";
25
+ export * from "./mobile";
26
+ export * from "./prompter";
27
+ export * from "./scanInfo";
6
28
  export * from "./selectModel";
29
+ export * from "./spinner";
7
30
  export * from "./streamAi";
8
- export * from "./executors";
9
- export * from "./dependencyScanner";
10
- export * from "./constants";
11
- export * from "./auth";
31
+ export * from "./transforms";
32
+ export * from "./typeChecker";
12
33
  export * from "./types";
13
- export * from "./capacitorApp";
14
- export * from "./extractDeps";
15
- export * from "./commandDecorators";
16
- export * from "./aiEditor";
17
- export * from "./builder";
18
- export * from "./spinner";
19
- export * from "./prompter";
20
- export * from "./guideline";
21
- export * from "./getDirname";
34
+ export * from "./ui";
35
+ export * from "./uploadRelease";
22
36
  export * from "./useStdoutDimensions";
23
- export * from "./scanInfo";
@@ -0,0 +1,25 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ or {
5
+ JsModuleSource() as $source where {
6
+ $source <: within JsImport(),
7
+ not $filename <: r".*\.(?:test|spec)\.tsx?",
8
+ $source <: r"\"@(?:apps|libs)/[^/]+/[^/]+/.+\"",
9
+ not $source <: r"\"@apps/[^/]+/env/env\.client\"",
10
+ register_diagnostic(
11
+ span = $source,
12
+ message = "@apps and @libs imports should only reference the first two path segments after the alias."
13
+ )
14
+ },
15
+ JsModuleSource() as $source where {
16
+ $source <: within JsImport(),
17
+ not $filename <: r".*\.(?:test|spec)\.tsx?",
18
+ $filename <: r".*apps/akasys/lib/projectBuild/[^/]+\.(?:constant|dictionary|document|service|signal|store)\.ts|.*apps/akasys/lib/projectBuild/[^/]+\.(?:Template|Unit|Util|View|Zone)\.tsx",
19
+ $source <: r"\"\.\./\.\./.*\"",
20
+ register_diagnostic(
21
+ span = $source,
22
+ message = "projectBuild module files should not import from two or more parent directories."
23
+ )
24
+ }
25
+ }
@@ -0,0 +1,32 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ or {
5
+ JsImport() as $import where {
6
+ $import <: contains `"react"`,
7
+ $import <: contains or {
8
+ `useState`,
9
+ `useEffect`,
10
+ `useContext`,
11
+ `useReducer`,
12
+ `useCallback`,
13
+ `useMemo`,
14
+ `useRef`,
15
+ `useImperativeHandle`,
16
+ `useLayoutEffect`,
17
+ `useDebugValue`
18
+ } as $hook,
19
+ register_diagnostic(
20
+ span = $hook,
21
+ message = "Client React hooks should not be imported in server files."
22
+ )
23
+ },
24
+ JsImport() as $import where {
25
+ $import <: not contains `"react"`,
26
+ $import <: contains `st` as $stImport,
27
+ register_diagnostic(
28
+ span = $stImport,
29
+ message = "Client store (st) should not be imported in server files."
30
+ )
31
+ }
32
+ }
@@ -0,0 +1,21 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ JsModuleSource() as $source where {
5
+ $source <: within JsImport(),
6
+ not $source <: or {
7
+ r"\"\..*",
8
+ r"\"akanjs.*",
9
+ r"\"@playwright.*",
10
+ r"\"@radix-ui.*",
11
+ r"\"@apps/.*",
12
+ r"\"@libs/.*",
13
+ r"\"@pkgs/.*",
14
+ r"\"bun:test\"",
15
+ r"\"react.*"
16
+ },
17
+ register_diagnostic(
18
+ span = $source,
19
+ message = "External libraries should not be imported. Only internal and allowed packages are permitted."
20
+ )
21
+ }