@akanjs/devkit 3.0.0-beta.2 → 3.0.0-beta.20
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.
- package/README.md +1 -0
- package/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +6 -8
- package/agentsIndex.test.ts +4 -7
- package/agentsIndex.ts +6 -22
- package/akanApp/BackendImportGraph.test.ts +8 -22
- package/akanApp/BackendImportGraph.ts +1 -20
- package/akanApp/akanApp.host.test.ts +1 -3
- package/akanApp/akanApp.host.ts +82 -254
- package/akanApp/devHostPolicy.test.ts +10 -34
- package/akanApp/devHostPolicy.ts +12 -98
- package/akanConfig/akanConfig.test.ts +85 -69
- package/akanConfig/akanConfig.ts +69 -153
- package/akanConfig/types.ts +1 -0
- package/akanContext.ts +106 -159
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.test.ts +5 -3
- package/applicationBuildRunner.ts +54 -61
- package/applicationReleasePackager.ts +24 -44
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +9 -30
- package/artifact/implicitRootLayout.ts +34 -106
- package/artifact/routeSeedIndex.test.ts +7 -6
- package/artifact/routeSeedIndex.ts +11 -44
- package/biome.base.json +6 -1
- package/biomeBase.ts +2 -5
- package/capacitorApp.test.ts +32 -16
- package/capacitorApp.ts +110 -238
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +77 -39
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +26 -5
- package/cloud/globalConfig.ts +70 -32
- package/codeAgent/agent/AkanCodeServices.ts +97 -0
- package/codeAgent/agent/AkanContextFiles.ts +94 -0
- package/codeAgent/agent/CodeAgent.ts +551 -0
- package/codeAgent/agent/CodeAgentAsks.ts +66 -0
- package/codeAgent/agent/CodeAgentEventMapper.ts +225 -0
- package/codeAgent/agent/CodeAgentGate.ts +93 -0
- package/codeAgent/agent/CodeAgentProxy.ts +82 -0
- package/codeAgent/agent/CodeAgentRpcHost.ts +103 -0
- package/codeAgent/agent/CodeAgentRpcListener.ts +109 -0
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +86 -0
- package/codeAgent/agent/CodeAgentSuspended.ts +85 -0
- package/codeAgent/agent/CodeAgentUi.ts +47 -0
- package/codeAgent/agent/CodeMailbox.ts +148 -0
- package/codeAgent/agent/CodeSessionFork.ts +56 -0
- package/codeAgent/agent/CodeSessionIndex.ts +78 -0
- package/codeAgent/agent/SubagentPool.ts +214 -0
- package/codeAgent/agent/akanCodeModel.ts +47 -0
- package/codeAgent/agent/akanCodePaths.ts +31 -0
- package/codeAgent/agent/akanContextFiles.test.ts +114 -0
- package/codeAgent/agent/akanSystemPrompt.ts +46 -0
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +523 -0
- package/codeAgent/agent/codeAgentProxy.test.ts +74 -0
- package/codeAgent/agent/codeAgentRpcListener.test.ts +113 -0
- package/codeAgent/agent/codeAgentSuspended.test.ts +47 -0
- package/codeAgent/agent/codeMailbox.test.ts +76 -0
- package/codeAgent/agent/codeSessionFork.test.ts +58 -0
- package/codeAgent/agent/codeSessionIndex.test.ts +77 -0
- package/codeAgent/agent/consoleToStderr.ts +8 -0
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +82 -0
- package/codeAgent/feedback/PreviewChrome.ts +82 -0
- package/codeAgent/feedback/PreviewFeedback.ts +46 -0
- package/codeAgent/feedback/PreviewView.ts +95 -0
- package/codeAgent/feedback/TurnFeedback.ts +98 -0
- package/codeAgent/feedback/VerifyFeedback.ts +40 -0
- package/codeAgent/feedback/previewChrome.test.ts +73 -0
- package/codeAgent/index.ts +34 -0
- package/codeAgent/skills/akan-code/SKILL.md +130 -0
- package/codeAgent/skills/akan-debug/SKILL.md +42 -0
- package/codeAgent/skills/akan-deps/SKILL.md +50 -0
- package/codeAgent/skills/akan-err/SKILL.md +39 -0
- package/codeAgent/skills/akan-module/SKILL.md +45 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +76 -0
- package/codeAgent/skills/akan-scalar/SKILL.md +52 -0
- package/codeAgent/skills/akan-service/SKILL.md +34 -0
- package/codeAgent/skills/akan-store/SKILL.md +67 -0
- package/codeAgent/skills/akan-ui/SKILL.md +63 -0
- package/codeAgent/skills/akan-validate/SKILL.md +44 -0
- package/codeAgent/tools/AkanCli.ts +50 -0
- package/codeAgent/tools/AkanCodePlugins.ts +136 -0
- package/codeAgent/tools/AkanEditScope.ts +48 -0
- package/codeAgent/tools/AkanEnvKeys.ts +50 -0
- package/codeAgent/tools/AkanToolPack.ts +92 -0
- package/codeAgent/tools/AkanVerifier.ts +48 -0
- package/codeAgent/tools/AskToolPack.ts +99 -0
- package/codeAgent/tools/MailToolPack.ts +76 -0
- package/codeAgent/tools/McpClient.ts +187 -0
- package/codeAgent/tools/McpOAuth.ts +233 -0
- package/codeAgent/tools/McpOAuthLoopback.ts +64 -0
- package/codeAgent/tools/McpServerConfig.ts +129 -0
- package/codeAgent/tools/McpSignIn.ts +158 -0
- package/codeAgent/tools/McpTokenStore.ts +81 -0
- package/codeAgent/tools/McpToolPack.ts +199 -0
- package/codeAgent/tools/SessionToolPack.ts +91 -0
- package/codeAgent/tools/WebToolPack.ts +147 -0
- package/codeAgent/tools/akanCodePlugins.test.ts +677 -0
- package/codeAgent/tools/askToolPack.test.ts +116 -0
- package/codeAgent/tools/mcpOAuth.test.ts +612 -0
- package/codeAgent/tools/mcpServerConfig.test.ts +212 -0
- package/codegenLock.test.ts +4 -13
- package/codegenLock.ts +3 -17
- package/commandDecorators/argMeta.ts +1 -5
- package/commandDecorators/camelToKebabCase.ts +1 -0
- package/commandDecorators/command.ts +46 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +2 -10
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +68 -145
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +31 -80
- package/devkitUtils.test.ts +48 -20
- package/executors.linkFallback.test.ts +103 -0
- package/executors.test.ts +132 -106
- package/executors.ts +194 -484
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +5 -3
- package/fileSys.ts +6 -8
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +27 -38
- package/frontendBuild/autoImportSync.ts +31 -65
- package/frontendBuild/buildRouteClient.test.ts +62 -37
- package/frontendBuild/bundleDefine.ts +12 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.ts +6 -41
- package/frontendBuild/csrArtifactBuilder.ts +6 -25
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +29 -78
- package/frontendBuild/cssImportResolver.ts +6 -19
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/fontOptimizer.test.ts +6 -14
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +46 -69
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +8 -32
- package/frontendBuild/hmrWatcher.ts +14 -71
- package/frontendBuild/pagesBundleBuilder.ts +4 -29
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/routeClientBuilder.ts +23 -46
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
- package/frontendBuild/styleContract.ts +1 -8
- package/frontendBuild/themeValidator.ts +4 -26
- package/frontendBuild/watchRootResolver.test.ts +23 -42
- package/frontendBuild/watchRootResolver.ts +5 -27
- package/getRelatedCnsts.ts +12 -92
- package/incrementalBuilder/buildBatch.proc.ts +22 -57
- package/incrementalBuilder/buildBatchProtocol.ts +8 -36
- package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
- package/incrementalBuilder/buildBatchRunner.ts +10 -31
- package/incrementalBuilder/builderChannel.test.ts +24 -49
- package/incrementalBuilder/builderChannel.ts +6 -37
- package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
- package/incrementalBuilder/builderRequestRouter.ts +5 -24
- package/incrementalBuilder/devWatchBatch.test.ts +21 -24
- package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
- package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +49 -161
- package/index.ts +5 -17
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +47 -194
- package/integration/devStabilityHarness.test.ts +13 -28
- package/integration/devStabilityHarness.ts +65 -330
- package/integration/ssrMemoryProbe.ts +41 -167
- package/libSource.test.ts +4 -15
- package/libSource.ts +19 -31
- package/lint/gritRules.test.ts +8 -24
- package/lint/no-deep-internal-import.grit +2 -2
- package/lint/no-redeclare-predefined-endpoint.grit +2 -1
- package/linter.test.ts +1 -2
- package/linter.ts +26 -116
- package/mobile/mobileTarget.ts +14 -13
- package/package.json +10 -5
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +46 -0
- package/platformTest/BunTestReport.test.ts +60 -0
- package/platformTest/BunTestReport.ts +65 -0
- package/platformTest/IdleWatchedProcess.test.ts +56 -0
- package/platformTest/IdleWatchedProcess.ts +73 -0
- package/platformTest/LinuxDockerTarget.ts +105 -0
- package/platformTest/PlatformTestRun.test.ts +64 -0
- package/platformTest/PlatformTestRun.ts +141 -0
- package/platformTest/PlatformTestTarget.ts +142 -0
- package/platformTest/TestSnapshot.test.ts +72 -0
- package/platformTest/TestSnapshot.ts +103 -0
- package/platformTest/WindowsSshTarget.ts +151 -0
- package/prompter.ts +3 -13
- package/qualityScanner.test.ts +9 -26
- package/qualityScanner.ts +197 -285
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.test.ts +0 -1
- package/routeSourceValidator.ts +7 -42
- package/scanInfo.ts +87 -216
- package/selectModel.ts +1 -2
- package/semver.ts +3 -5
- package/slicePlanner.test.ts +6 -24
- package/slicePlanner.ts +11 -32
- package/spinner.test.ts +3 -8
- package/spinner.ts +4 -16
- package/sshTunnel.ts +1 -6
- package/ssrScanner.test.ts +1 -6
- package/ssrScanner.ts +103 -113
- package/stripAnsi.ts +3 -0
- package/subspace.test.ts +30 -25
- package/subspace.ts +81 -115
- package/subspaceConfig.ts +11 -9
- package/testHelpers.ts +194 -0
- package/transforms/asyncDefaultExportDetector.ts +3 -11
- package/transforms/barrelAnalyzer.ts +43 -135
- package/transforms/barrelImportsPlugin.ts +38 -175
- package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
- package/transforms/externalizeFrameworkPlugin.ts +13 -87
- package/transforms/moduleSyntax.ts +32 -0
- package/transforms/rscUseClientTransform.ts +35 -20
- package/transforms/transforms.test.ts +153 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +2 -22
- package/typeChecker.ts +7 -63
- package/types.ts +1 -0
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +101 -110
- package/workflow/index.ts +0 -1
- package/workflow/moduleIndex.test.ts +4 -15
- package/workflow/moduleIndex.ts +9 -49
- package/workflow/plan.ts +18 -25
- package/workflow/render.ts +80 -96
- package/workflow/rolloutGate.test.ts +1 -1
- package/workflow/source.ts +52 -69
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.ts +4 -19
- package/abstractCompactor.ts +0 -112
- package/aiEditor.test.ts +0 -60
- package/aiEditor.ts +0 -387
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/guideline.ts +0 -19
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
|
@@ -26,18 +26,25 @@ const mockSpawns = (): SpawnRecord[] => {
|
|
|
26
26
|
return spawns;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
|
+
const createHost = (onMessage: (message: unknown) => void = () => undefined) =>
|
|
30
|
+
new IncrementalBuilderHost({ app: { cwdPath: "/tmp/app" } as never, entry: "/tmp/builder.ts", env: {}, onMessage });
|
|
31
|
+
|
|
32
|
+
const startRecordingHost = () => {
|
|
33
|
+
const spawns = mockSpawns();
|
|
34
|
+
const messages: unknown[] = [];
|
|
35
|
+
const host = createHost((message) => messages.push(message));
|
|
36
|
+
host.start();
|
|
37
|
+
spawns[0]?.options.ipc?.({ type: "builder-ready" });
|
|
38
|
+
return { spawns, messages, host };
|
|
39
|
+
};
|
|
40
|
+
|
|
29
41
|
describe("IncrementalBuilderHost", () => {
|
|
30
42
|
test("restarts after a ready builder exits", async () => {
|
|
31
43
|
const spawns = mockSpawns();
|
|
32
44
|
|
|
33
45
|
const onReady = mock();
|
|
34
46
|
const onRestartReady = mock();
|
|
35
|
-
const host =
|
|
36
|
-
app: { cwdPath: "/tmp/app" } as never,
|
|
37
|
-
entry: "/tmp/builder.ts",
|
|
38
|
-
env: {},
|
|
39
|
-
onMessage: () => undefined,
|
|
40
|
-
});
|
|
47
|
+
const host = createHost();
|
|
41
48
|
|
|
42
49
|
host.start({ onReady, onRestartReady });
|
|
43
50
|
spawns[0]?.options.ipc?.({ type: "builder-ready" });
|
|
@@ -60,12 +67,7 @@ describe("IncrementalBuilderHost", () => {
|
|
|
60
67
|
test("recycles a ready builder gracefully and replaces it immediately", async () => {
|
|
61
68
|
const spawns = mockSpawns();
|
|
62
69
|
const onRestartReady = mock();
|
|
63
|
-
const host =
|
|
64
|
-
app: { cwdPath: "/tmp/app" } as never,
|
|
65
|
-
entry: "/tmp/builder.ts",
|
|
66
|
-
env: {},
|
|
67
|
-
onMessage: () => undefined,
|
|
68
|
-
});
|
|
70
|
+
const host = createHost();
|
|
69
71
|
|
|
70
72
|
host.start({ onRestartReady });
|
|
71
73
|
spawns[0]?.options.ipc?.({ type: "builder-ready" });
|
|
@@ -73,18 +75,14 @@ describe("IncrementalBuilderHost", () => {
|
|
|
73
75
|
|
|
74
76
|
const reason = "rss=1300MiB>=1200MiB after 3 build(s)";
|
|
75
77
|
expect(host.recycle(reason)).toBe(true);
|
|
76
|
-
// Graceful: the builder is asked to drain, not killed, so a rebuild in flight still completes.
|
|
77
78
|
expect(spawns[0]?.proc.send).toHaveBeenCalledWith({ type: "builder-shutdown", reason });
|
|
78
79
|
expect(spawns[0]?.proc.kill).not.toHaveBeenCalled();
|
|
79
80
|
expect(host.recycle("second request")).toBe(false);
|
|
80
81
|
|
|
81
|
-
// The drain refuses everything that arrives during it, so a request sent here is a request the
|
|
82
|
-
// developer gets an error page for. Reporting the state is what lets the host hold it instead.
|
|
83
82
|
expect(host.status).toBe("recycling");
|
|
84
83
|
expect(host.send({ type: "build-route", id: 9, routeId: "z", seeds: [], knownEntries: [] })).toBe(false);
|
|
85
84
|
expect(spawns[0]?.proc.send).toHaveBeenCalledTimes(1);
|
|
86
85
|
|
|
87
|
-
// A planned exit skips the crash backoff — the dev server has no file watcher until it is back.
|
|
88
86
|
spawns[0]?.options.onExit?.();
|
|
89
87
|
expect(spawns).toHaveLength(2);
|
|
90
88
|
expect(spawns[1]?.options.env?.AKAN_BUILDER_ANNOUNCE_BOOT).toBe("1");
|
|
@@ -103,17 +101,7 @@ describe("IncrementalBuilderHost", () => {
|
|
|
103
101
|
});
|
|
104
102
|
|
|
105
103
|
test("fails the requests a departing builder never answered", async () => {
|
|
106
|
-
const spawns =
|
|
107
|
-
const messages: unknown[] = [];
|
|
108
|
-
const host = new IncrementalBuilderHost({
|
|
109
|
-
app: { cwdPath: "/tmp/app" } as never,
|
|
110
|
-
entry: "/tmp/builder.ts",
|
|
111
|
-
env: {},
|
|
112
|
-
onMessage: (message) => messages.push(message),
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
host.start();
|
|
116
|
-
spawns[0]?.options.ipc?.({ type: "builder-ready" });
|
|
104
|
+
const { spawns, messages, host } = startRecordingHost();
|
|
117
105
|
expect(host.send({ type: "build-route", id: 1, routeId: "a", seeds: [], knownEntries: [] })).toBe(true);
|
|
118
106
|
expect(host.send({ type: "build-csr", id: 2, reason: "device webview" })).toBe(true);
|
|
119
107
|
// Answered before the exit, so this one must not be failed again afterwards.
|
|
@@ -124,8 +112,6 @@ describe("IncrementalBuilderHost", () => {
|
|
|
124
112
|
host.recycle("rss=1300MiB>=1200MiB after 3 build(s)");
|
|
125
113
|
spawns[0]?.options.onExit?.();
|
|
126
114
|
|
|
127
|
-
// Nothing else answers these: the builder only refuses requests that arrive after it starts shutting
|
|
128
|
-
// down, and a kill or a truncated write sends nothing at all.
|
|
129
115
|
expect(messages).toEqual([
|
|
130
116
|
{
|
|
131
117
|
type: "build-route-res",
|
|
@@ -152,17 +138,7 @@ describe("IncrementalBuilderHost", () => {
|
|
|
152
138
|
});
|
|
153
139
|
|
|
154
140
|
test("names a crash rather than a recycle, and answers on stop too", async () => {
|
|
155
|
-
const spawns =
|
|
156
|
-
const messages: unknown[] = [];
|
|
157
|
-
const host = new IncrementalBuilderHost({
|
|
158
|
-
app: { cwdPath: "/tmp/app" } as never,
|
|
159
|
-
entry: "/tmp/builder.ts",
|
|
160
|
-
env: {},
|
|
161
|
-
onMessage: (message) => messages.push(message),
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
host.start();
|
|
165
|
-
spawns[0]?.options.ipc?.({ type: "builder-ready" });
|
|
141
|
+
const { spawns, messages, host } = startRecordingHost();
|
|
166
142
|
host.send({ type: "build-route", id: 1, routeId: "a", seeds: [], knownEntries: [] });
|
|
167
143
|
messages.length = 0;
|
|
168
144
|
spawns[0]?.options.onExit?.();
|
|
@@ -175,8 +151,6 @@ describe("IncrementalBuilderHost", () => {
|
|
|
175
151
|
},
|
|
176
152
|
]);
|
|
177
153
|
|
|
178
|
-
// `stop()` clears the process before its exit callback runs, so the callback bails on its identity
|
|
179
|
-
// check and cannot be the only place this happens.
|
|
180
154
|
await wait(1_050);
|
|
181
155
|
spawns[1]?.options.ipc?.({ type: "builder-ready" });
|
|
182
156
|
messages.length = 0;
|
|
@@ -189,12 +163,7 @@ describe("IncrementalBuilderHost", () => {
|
|
|
189
163
|
|
|
190
164
|
test("only recycles a builder that is ready", () => {
|
|
191
165
|
const spawns = mockSpawns();
|
|
192
|
-
const host =
|
|
193
|
-
app: { cwdPath: "/tmp/app" } as never,
|
|
194
|
-
entry: "/tmp/builder.ts",
|
|
195
|
-
env: {},
|
|
196
|
-
onMessage: () => undefined,
|
|
197
|
-
});
|
|
166
|
+
const host = createHost();
|
|
198
167
|
|
|
199
168
|
host.start();
|
|
200
169
|
expect(host.recycle("while still booting")).toBe(false);
|
|
@@ -222,11 +191,7 @@ describe("IncrementalBuilderHost.maxRssBytes", () => {
|
|
|
222
191
|
}
|
|
223
192
|
};
|
|
224
193
|
|
|
225
|
-
|
|
226
|
-
* Only where nothing else supplies a limit, which is not true in a container: a cgroup `memory.max`
|
|
227
|
-
* makes `resolveMaxRssBytes` derive from that instead. Measured under `docker --memory=7g`, this
|
|
228
|
-
* returned 2.45GiB rather than the fallback — the assertion was about the runner, not the code.
|
|
229
|
-
*/
|
|
194
|
+
// Skipped under a cgroup `memory.max`, which `resolveMaxRssBytes` derives the ceiling from instead.
|
|
230
195
|
test.skipIf(MemoryLimit.readCgroupBytes() !== null)("defaults to a dev ceiling well above a fresh boot", () => {
|
|
231
196
|
withEnv(
|
|
232
197
|
{ AKAN_BUILDER_MAX_RSS_MB: undefined, AKAN_BUILDER_MAX_RSS: undefined, AKAN_MEMORY_LIMIT: undefined },
|
|
@@ -237,8 +202,6 @@ describe("IncrementalBuilderHost.maxRssBytes", () => {
|
|
|
237
202
|
});
|
|
238
203
|
|
|
239
204
|
test("derives the ceiling from the sandbox's own limit, wherever it runs", () => {
|
|
240
|
-
// The property the fallback test cannot assert in a container, stated so it holds on every runner:
|
|
241
|
-
// the builder gets 35% of whatever the sandbox is allowed.
|
|
242
205
|
withEnv(
|
|
243
206
|
{
|
|
244
207
|
AKAN_BUILDER_MAX_RSS_MB: undefined,
|
|
@@ -14,12 +14,7 @@ const builderMsgTypeSet = new Set<BuilderMessage["type"]>([
|
|
|
14
14
|
"build-status",
|
|
15
15
|
"builder-metrics",
|
|
16
16
|
]);
|
|
17
|
-
/**
|
|
18
|
-
* Where a dev child process writes. `"pipe"` is not only about keeping a TUI's frame clean: a Bun child
|
|
19
|
-
* that inherits the controlling terminal snapshots its termios at spawn and writes that snapshot back
|
|
20
|
-
* when it exits, so a builder spawned while something holds the terminal raw turns it raw again on the
|
|
21
|
-
* first recycle. See the XXX comment on `Spinner.oraOptions`.
|
|
22
|
-
*/
|
|
17
|
+
/** Prefer `"pipe"` under a TUI: a Bun child that inherits the terminal restores its spawn-time termios on exit. */
|
|
23
18
|
export type DevStdioMode = "inherit" | "pipe";
|
|
24
19
|
|
|
25
20
|
interface IncrementalBuilderHostOptions {
|
|
@@ -28,48 +23,28 @@ interface IncrementalBuilderHostOptions {
|
|
|
28
23
|
env: Record<string, string>;
|
|
29
24
|
stdio?: DevStdioMode;
|
|
30
25
|
onMessage: (message: BuilderMessage) => void;
|
|
31
|
-
/**
|
|
32
|
-
* Required when `stdio` is `"pipe"`: an undrained pipe fills and blocks the builder's next write.
|
|
33
|
-
* Text arrives as decoded chunks, not lines — a consumer that needs lines splits them itself.
|
|
34
|
-
*/
|
|
26
|
+
/** Required with `stdio: "pipe"` (an undrained pipe blocks the builder); receives decoded chunks, not lines. */
|
|
35
27
|
onOutput?: (kind: "stdout" | "stderr", text: string) => void;
|
|
36
28
|
}
|
|
37
29
|
|
|
38
|
-
/**
|
|
39
|
-
* `recycling` is the drain: the builder is still alive and still holds the work it accepted, but it
|
|
40
|
-
* refuses anything new. Saying so here is what lets the host hold those requests for the replacement
|
|
41
|
-
* instead of handing the developer the refusal.
|
|
42
|
-
*/
|
|
30
|
+
/** `recycling`: the builder finishes accepted work but refuses new requests; hold them for the replacement. */
|
|
43
31
|
export type IncrementalBuilderStatus = "starting" | "ready" | "recycling" | "restarting" | "stopped";
|
|
44
32
|
|
|
45
33
|
interface IncrementalBuilderStartOptions {
|
|
46
34
|
onExit?: () => void;
|
|
47
35
|
onReady?: () => void;
|
|
48
36
|
onRestartReady?: () => void;
|
|
49
|
-
/**
|
|
50
|
-
* The builder is gone and a replacement is on its way. Distinct from `onExit`, which reports the
|
|
51
|
-
* builder giving up: this one says the dev server is temporarily without a watcher.
|
|
52
|
-
*/
|
|
37
|
+
/** The builder is gone and a replacement is coming (unwatched until then); `onExit` means it gave up. */
|
|
53
38
|
onAway?: () => void;
|
|
54
|
-
/**
|
|
55
|
-
* Ask the builder to re-announce the artifact it boots with. Needed whenever a *previous* builder's
|
|
56
|
-
* artifact may still be live in a running backend — after an rss recycle, and after an idle wake.
|
|
57
|
-
*/
|
|
39
|
+
/** Re-announce the boot artifact, for when a previous builder's artifact may still be live in the backend. */
|
|
58
40
|
announceBootState?: boolean;
|
|
59
41
|
}
|
|
60
42
|
|
|
61
43
|
export class IncrementalBuilderHost {
|
|
62
44
|
static readonly #restartBaseDelayMs = 1_000;
|
|
63
45
|
static readonly #restartMaxDelayMs = 30_000;
|
|
64
|
-
/**
|
|
65
|
-
* A builder that has stopped answering has to be replaced anyway; killing it after this long turns
|
|
66
|
-
* a wedged drain into an ordinary restart instead of leaving the recycle stuck forever.
|
|
67
|
-
*/
|
|
68
46
|
static readonly #recycleDrainTimeoutMs = 30_000;
|
|
69
|
-
|
|
70
|
-
* Dev has no memory limit to derive a fraction from, so this is what bounds the builder there. Well
|
|
71
|
-
* above a fresh boot (~300-600MB depending on app size) with room for one full rebuild on top.
|
|
72
|
-
*/
|
|
47
|
+
// A fresh boot is ~300-600MB; this leaves room for one full rebuild on top.
|
|
73
48
|
static readonly #devMaxRssBytes = 1_200 * 1024 * 1024;
|
|
74
49
|
logger = new Logger("IncrementalBuilderHost");
|
|
75
50
|
entry: string;
|
|
@@ -87,16 +62,8 @@ export class IncrementalBuilderHost {
|
|
|
87
62
|
#recycleRequested: boolean = false;
|
|
88
63
|
#spawnAfterRecycle: boolean = false;
|
|
89
64
|
#manualStop = false;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
* correlation id.
|
|
93
|
-
*
|
|
94
|
-
* Nothing else answers a request whose builder exits while holding it: the builder only refuses
|
|
95
|
-
* requests that arrive *after* it starts shutting down, a kill or crash sends nothing at all, and even
|
|
96
|
-
* a clean drain races its own `process.exit`. The builder exits routinely — it is recycled whenever its
|
|
97
|
-
* RSS passes the ceiling — so a page request that happened to be mid route-build left the backend's
|
|
98
|
-
* promise pending and the browser tab spinning with no error and nothing to retry.
|
|
99
|
-
*/
|
|
65
|
+
// Nothing else answers a request whose builder exits holding it: a crash or kill sends nothing, and a drain races
|
|
66
|
+
// its own exit, so an unanswered page request would spin forever.
|
|
100
67
|
readonly #inFlight = new Map<number, "build-route" | "build-csr">();
|
|
101
68
|
#startOptions: IncrementalBuilderStartOptions = {};
|
|
102
69
|
constructor({ app, entry, env, stdio = "inherit", onMessage, onOutput }: IncrementalBuilderHostOptions) {
|
|
@@ -110,11 +77,7 @@ export class IncrementalBuilderHost {
|
|
|
110
77
|
get status() {
|
|
111
78
|
return this.#status;
|
|
112
79
|
}
|
|
113
|
-
/**
|
|
114
|
-
* The running builder's pid, so the host can read its RSS from the OS between builds. The builder
|
|
115
|
-
* only reports its own metrics at work-completion points, which is the *peak*; on a platform that
|
|
116
|
-
* returns bundler arenas to the OS while idle, that sample goes stale within seconds.
|
|
117
|
-
*/
|
|
80
|
+
/** For reading RSS between builds: the builder's own metrics sample the post-work peak, stale once arenas return. */
|
|
118
81
|
get pid(): number | null {
|
|
119
82
|
return this.#proc?.pid ?? null;
|
|
120
83
|
}
|
|
@@ -129,8 +92,6 @@ export class IncrementalBuilderHost {
|
|
|
129
92
|
#spawn(isRestart: boolean) {
|
|
130
93
|
this.#status = isRestart ? "restarting" : "starting";
|
|
131
94
|
this.ready = false;
|
|
132
|
-
// A fresh builder rebuilds every artifact while the running backend still holds the previous one;
|
|
133
|
-
// the flag is what tells it to re-announce what it booted with.
|
|
134
95
|
const afterRecycle = this.#spawnAfterRecycle;
|
|
135
96
|
this.#spawnAfterRecycle = false;
|
|
136
97
|
let proc!: Bun.Subprocess<"ignore", "inherit" | "pipe", "inherit" | "pipe">;
|
|
@@ -170,11 +131,9 @@ export class IncrementalBuilderHost {
|
|
|
170
131
|
this.#startOptions.onExit?.();
|
|
171
132
|
return;
|
|
172
133
|
}
|
|
173
|
-
//
|
|
174
|
-
// has primed its own index — and an edit that lands in that window is reported by nobody.
|
|
134
|
+
// Both branches below leave the tree unwatched until the replacement primes its index.
|
|
175
135
|
this.#startOptions.onAway?.();
|
|
176
|
-
// A
|
|
177
|
-
// crash backoff — the dev server is without a watcher until the replacement is up.
|
|
136
|
+
// A planned exit: no failed attempt, no crash backoff.
|
|
178
137
|
if (wasRecycle) {
|
|
179
138
|
this.logger.verbose("builder exited for a recycle; spawning its replacement now");
|
|
180
139
|
this.#spawnAfterRecycle = true;
|
|
@@ -220,20 +179,14 @@ export class IncrementalBuilderHost {
|
|
|
220
179
|
this.#spawn(true);
|
|
221
180
|
}, delay);
|
|
222
181
|
}
|
|
223
|
-
/**
|
|
224
|
-
* Ask the builder to drain its queues and exit so the OS reclaims the bundler arenas `Bun.build`
|
|
225
|
-
* never gives back; `onExit` then spawns the replacement. Graceful rather than `kill()` so a rebuild
|
|
226
|
-
* in flight is never truncated, with a watchdog for a builder that stops answering.
|
|
227
|
-
*/
|
|
182
|
+
/** Drain-and-exit rather than `kill()`, so no rebuild is truncated; false when the request was not sent. */
|
|
228
183
|
recycle(reason: string): boolean {
|
|
229
184
|
if (!this.#proc || this.#status !== "ready" || this.#recycleRequested) return false;
|
|
230
185
|
const proc = this.#proc;
|
|
231
186
|
if (!this.send({ type: "builder-shutdown", reason })) return false;
|
|
232
187
|
this.#recycleRequested = true;
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
// one at a time, into the dev error page a recycle is supposed to be invisible to. `ready` the
|
|
236
|
-
// field is deliberately untouched: `onExit` reads it to tell a planned exit from a boot failure.
|
|
188
|
+
// Not `ready`: the draining builder refuses new requests. The `ready` field stays, since `onExit` reads it to tell
|
|
189
|
+
// a planned exit from a boot failure.
|
|
237
190
|
this.#status = "recycling";
|
|
238
191
|
this.logger.debug(`recycling builder pid=${proc.pid} (${reason})`);
|
|
239
192
|
this.#recycleTimer = setTimeout(() => {
|
|
@@ -252,11 +205,7 @@ export class IncrementalBuilderHost {
|
|
|
252
205
|
clearTimeout(this.#recycleTimer);
|
|
253
206
|
this.#recycleTimer = null;
|
|
254
207
|
}
|
|
255
|
-
/**
|
|
256
|
-
* RSS at which the builder is recycled: an explicit override, else a share of the container's limit
|
|
257
|
-
* (the builder is one of several dev processes), else the dev default. Set
|
|
258
|
-
* `AKAN_BUILDER_MAX_RSS_MB=0` to leave it unbounded.
|
|
259
|
-
*/
|
|
208
|
+
/** RSS that triggers a recycle; `null` (`AKAN_BUILDER_MAX_RSS_MB=0`) means unbounded. */
|
|
260
209
|
static maxRssBytes(): number | null {
|
|
261
210
|
if (process.env.AKAN_BUILDER_MAX_RSS_MB === "0") return null;
|
|
262
211
|
return MemoryLimit.resolveMaxRssBytes({
|
|
@@ -268,8 +217,7 @@ export class IncrementalBuilderHost {
|
|
|
268
217
|
}
|
|
269
218
|
send(message: BuilderMessage): boolean {
|
|
270
219
|
if (!this.#proc || this.#status !== "ready") {
|
|
271
|
-
//
|
|
272
|
-
// states nothing is recovering from are worth a warning.
|
|
220
|
+
// Recycling/restarting is routine (the caller holds the message), so only unrecoverable states warn.
|
|
273
221
|
if (this.#status === "recycling" || this.#status === "restarting")
|
|
274
222
|
this.logger.verbose(`incrementalBuilderHost is ${this.#status}; ${message.type} is for the replacement`);
|
|
275
223
|
else this.logger.warn(`incrementalBuilderHost is ${this.#status}; cannot send ${message.type}`);
|
|
@@ -286,19 +234,13 @@ export class IncrementalBuilderHost {
|
|
|
286
234
|
return false;
|
|
287
235
|
}
|
|
288
236
|
}
|
|
289
|
-
|
|
290
|
-
* Answer every request the departing builder still owed, as if it had failed them itself. `onExit`
|
|
291
|
-
* cannot do this alone: `stop()` clears `#proc` first, so the exit callback bails on its identity check.
|
|
292
|
-
*/
|
|
237
|
+
// Called from `stop()` too: it clears `#proc` first, so `onExit` bails on its identity check.
|
|
293
238
|
#failInFlight(reason: string): void {
|
|
294
239
|
if (!this.#inFlight.size) return;
|
|
295
240
|
const lost = [...this.#inFlight];
|
|
296
241
|
this.#inFlight.clear();
|
|
297
242
|
this.logger.warn(`failing ${lost.length} unanswered builder request(s): ${reason}`);
|
|
298
|
-
for (const [id, type] of lost) {
|
|
299
|
-
if (type === "build-route") this.#onMessage({ type: "build-route-res", id, ok: false, error: reason });
|
|
300
|
-
else this.#onMessage({ type: "build-csr-res", id, ok: false, error: reason });
|
|
301
|
-
}
|
|
243
|
+
for (const [id, type] of lost) this.#onMessage({ type: `${type}-res`, id, ok: false, error: reason });
|
|
302
244
|
}
|
|
303
245
|
stop() {
|
|
304
246
|
this.#manualStop = true;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { BuilderMessage, BuilderReq } from "akanjs/server";
|
|
4
|
+
import { createTempApp, tempRoots, writeText } from "../testHelpers";
|
|
5
|
+
|
|
6
|
+
const track = tempRoots();
|
|
7
|
+
|
|
8
|
+
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
+
|
|
10
|
+
const until = async (proc: Bun.Subprocess, what: string, condition: () => boolean) => {
|
|
11
|
+
const deadline = Date.now() + 20_000;
|
|
12
|
+
while (!condition()) {
|
|
13
|
+
if (proc.exitCode !== null || Date.now() > deadline) throw new Error(`the builder never sent ${what}`);
|
|
14
|
+
await wait(20);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
describe("incremental builder process", () => {
|
|
19
|
+
test("says why and where a route's client bundle failed, in its answer, its build status and its log", async () => {
|
|
20
|
+
const { root } = track(await createTempApp("demo"));
|
|
21
|
+
const appDir = path.join(root, "apps/demo");
|
|
22
|
+
// The batch runner looks under the workspace root first: a stand-in for the boot build, which needs a whole app.
|
|
23
|
+
await writeText(
|
|
24
|
+
path.join(root, "pkgs/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
|
|
25
|
+
`const { generation } = JSON.parse(process.argv[2]);
|
|
26
|
+
process.send({ type: "build-batch-result", data: { generation, errors: {}, artifact: {}, optimizedFonts: { css: "", files: [] } } });
|
|
27
|
+
`,
|
|
28
|
+
);
|
|
29
|
+
const page = path.join(appDir, "page/_index.tsx");
|
|
30
|
+
await writeText(page, 'import { Broken } from "../ui/Broken";\nexport default Broken;\n');
|
|
31
|
+
await writeText(
|
|
32
|
+
path.join(appDir, "ui/Broken.tsx"),
|
|
33
|
+
'"use client";\nimport { gone } from "./not-there";\nexport const Broken = () => gone;\n',
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const messages: BuilderMessage[] = [];
|
|
37
|
+
const proc = Bun.spawn(["bun", path.join(import.meta.dir, "incrementalBuilder.proc.ts")], {
|
|
38
|
+
cwd: appDir,
|
|
39
|
+
env: {
|
|
40
|
+
...process.env,
|
|
41
|
+
AKAN_WORKSPACE_ROOT: root,
|
|
42
|
+
AKAN_PUBLIC_APP_NAME: "demo",
|
|
43
|
+
AKAN_PUBLIC_REPO_NAME: "repo",
|
|
44
|
+
AKAN_PUBLIC_SERVE_DOMAIN: "localhost",
|
|
45
|
+
AKAN_PUBLIC_ENV: "local",
|
|
46
|
+
AKAN_WATCH: "0",
|
|
47
|
+
},
|
|
48
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
49
|
+
serialization: "advanced",
|
|
50
|
+
ipc: (message: BuilderMessage) => {
|
|
51
|
+
messages.push(message);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
try {
|
|
55
|
+
await until(proc, "builder-ready", () => messages.some((message) => message.type === "builder-ready"));
|
|
56
|
+
proc.send({
|
|
57
|
+
type: "build-route",
|
|
58
|
+
id: 1,
|
|
59
|
+
routeId: "page:/",
|
|
60
|
+
seeds: [page],
|
|
61
|
+
knownEntries: [],
|
|
62
|
+
generation: 3,
|
|
63
|
+
} satisfies BuilderReq);
|
|
64
|
+
await until(proc, "build-route-res", () => messages.some((message) => message.type === "build-route-res"));
|
|
65
|
+
} finally {
|
|
66
|
+
proc.kill();
|
|
67
|
+
}
|
|
68
|
+
const log = await new Response(proc.stderr).text();
|
|
69
|
+
|
|
70
|
+
const reason = '"./not-there" (apps/demo/ui/Broken.tsx:2:22)';
|
|
71
|
+
expect(messages.find((message) => message.type === "build-route-res")).toMatchObject({
|
|
72
|
+
id: 1,
|
|
73
|
+
ok: false,
|
|
74
|
+
error: expect.stringContaining(reason),
|
|
75
|
+
});
|
|
76
|
+
expect(messages.find((message) => message.type === "build-status")).toMatchObject({
|
|
77
|
+
data: { generation: 3, phase: "route", ok: false, message: expect.stringContaining(reason) },
|
|
78
|
+
});
|
|
79
|
+
expect(log).toContain(reason);
|
|
80
|
+
}, 30_000);
|
|
81
|
+
});
|