@akanjs/devkit 3.0.0-beta.19 → 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/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +1 -4
- 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 +64 -250
- package/akanApp/devHostPolicy.test.ts +10 -34
- package/akanApp/devHostPolicy.ts +12 -98
- package/akanConfig/akanConfig.test.ts +30 -39
- package/akanConfig/akanConfig.ts +40 -136
- package/akanContext.ts +106 -159
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.ts +25 -59
- package/applicationReleasePackager.ts +24 -44
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +3 -19
- package/artifact/implicitRootLayout.ts +34 -91
- package/artifact/routeSeedIndex.ts +11 -44
- package/biomeBase.ts +2 -5
- package/capacitorApp.test.ts +4 -14
- package/capacitorApp.ts +86 -235
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +33 -57
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +3 -2
- package/cloud/globalConfig.ts +64 -31
- package/codeAgent/agent/AkanCodeServices.ts +15 -57
- package/codeAgent/agent/AkanContextFiles.ts +5 -28
- package/codeAgent/agent/CodeAgent.ts +33 -108
- package/codeAgent/agent/CodeAgentAsks.ts +7 -30
- package/codeAgent/agent/CodeAgentEventMapper.ts +9 -52
- package/codeAgent/agent/CodeAgentGate.ts +8 -34
- package/codeAgent/agent/CodeAgentProxy.ts +4 -13
- package/codeAgent/agent/CodeAgentRpcHost.ts +3 -16
- package/codeAgent/agent/CodeAgentRpcListener.ts +2 -10
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +4 -15
- package/codeAgent/agent/CodeAgentSuspended.ts +1 -8
- package/codeAgent/agent/CodeAgentUi.ts +2 -7
- package/codeAgent/agent/CodeMailbox.ts +7 -34
- package/codeAgent/agent/CodeSessionFork.ts +4 -19
- package/codeAgent/agent/CodeSessionIndex.ts +5 -31
- package/codeAgent/agent/SubagentPool.ts +48 -58
- package/codeAgent/agent/akanCodeModel.ts +5 -39
- package/codeAgent/agent/akanCodePaths.ts +4 -24
- package/codeAgent/agent/akanContextFiles.test.ts +0 -2
- package/codeAgent/agent/akanSystemPrompt.ts +1 -9
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +5 -28
- package/codeAgent/agent/codeMailbox.test.ts +0 -4
- package/codeAgent/agent/codeSessionFork.test.ts +0 -2
- package/codeAgent/agent/codeSessionIndex.test.ts +9 -4
- package/codeAgent/agent/consoleToStderr.ts +2 -12
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +3 -18
- package/codeAgent/feedback/PreviewChrome.ts +5 -29
- package/codeAgent/feedback/PreviewFeedback.ts +2 -9
- package/codeAgent/feedback/PreviewView.ts +3 -15
- package/codeAgent/feedback/TurnFeedback.ts +2 -17
- package/codeAgent/feedback/VerifyFeedback.ts +1 -7
- package/codeAgent/feedback/previewChrome.test.ts +1 -1
- package/codeAgent/index.ts +2 -10
- package/codeAgent/skills/akan-code/SKILL.md +12 -4
- package/codeAgent/tools/AkanCli.ts +4 -16
- package/codeAgent/tools/AkanCodePlugins.ts +21 -43
- package/codeAgent/tools/AkanEditScope.ts +2 -12
- package/codeAgent/tools/AkanEnvKeys.ts +3 -11
- package/codeAgent/tools/AkanToolPack.ts +6 -28
- package/codeAgent/tools/AkanVerifier.ts +1 -8
- package/codeAgent/tools/AskToolPack.ts +3 -15
- package/codeAgent/tools/MailToolPack.ts +0 -9
- package/codeAgent/tools/McpClient.ts +3 -24
- package/codeAgent/tools/McpOAuth.ts +6 -41
- package/codeAgent/tools/McpOAuthLoopback.ts +2 -15
- package/codeAgent/tools/McpServerConfig.ts +7 -26
- package/codeAgent/tools/McpSignIn.ts +46 -49
- package/codeAgent/tools/McpTokenStore.ts +6 -17
- package/codeAgent/tools/McpToolPack.ts +107 -35
- package/codeAgent/tools/SessionToolPack.ts +1 -11
- package/codeAgent/tools/WebToolPack.ts +1 -7
- package/codeAgent/tools/akanCodePlugins.test.ts +231 -63
- package/codeAgent/tools/askToolPack.test.ts +0 -3
- package/codeAgent/tools/mcpOAuth.test.ts +315 -27
- package/codeAgent/tools/mcpServerConfig.test.ts +21 -14
- 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 +44 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +1 -9
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +66 -144
- 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 +27 -78
- package/devkitUtils.test.ts +47 -19
- package/executors.test.ts +114 -60
- package/executors.ts +165 -484
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +1 -1
- package/fileSys.ts +6 -8
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +14 -38
- package/frontendBuild/autoImportSync.ts +15 -67
- package/frontendBuild/buildRouteClient.test.ts +10 -36
- 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 +5 -18
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/fontOptimizer.test.ts +5 -13
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +27 -52
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +7 -31
- package/frontendBuild/hmrWatcher.ts +14 -71
- package/frontendBuild/pagesBundleBuilder.ts +4 -29
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/routeClientBuilder.ts +7 -47
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/sourceMtimeIndex.test.ts +37 -94
- 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 +14 -18
- 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 +48 -161
- package/index.ts +3 -15
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +47 -194
- package/integration/devStabilityHarness.test.ts +9 -29
- 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 +6 -23
- package/linter.test.ts +1 -2
- package/linter.ts +22 -115
- package/mobile/mobileTarget.ts +14 -13
- package/package.json +2 -2
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +3 -14
- package/platformTest/IdleWatchedProcess.test.ts +5 -13
- package/platformTest/IdleWatchedProcess.ts +1 -5
- package/platformTest/LinuxDockerTarget.ts +20 -35
- package/platformTest/PlatformTestRun.test.ts +5 -9
- package/platformTest/PlatformTestTarget.ts +24 -2
- package/platformTest/TestSnapshot.test.ts +6 -14
- package/platformTest/TestSnapshot.ts +1 -5
- package/platformTest/WindowsSshTarget.ts +6 -22
- package/prompter.ts +3 -6
- package/qualityScanner.test.ts +6 -24
- package/qualityScanner.ts +196 -284
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.ts +7 -30
- package/scanInfo.ts +82 -214
- 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/subspace.test.ts +6 -25
- package/subspace.ts +31 -109
- package/subspaceConfig.ts +3 -11
- package/testHelpers.ts +49 -34
- 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/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +85 -146
- 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/source.ts +33 -56
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.ts +4 -19
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
|
@@ -28,6 +28,13 @@ import {
|
|
|
28
28
|
shouldWarnBuilderRssCeilingTight,
|
|
29
29
|
} from "./devHostPolicy";
|
|
30
30
|
|
|
31
|
+
const status = (phase: BuildPhase, generation: number, ok: boolean): DevBuildStatus => ({
|
|
32
|
+
generation,
|
|
33
|
+
phase,
|
|
34
|
+
ok,
|
|
35
|
+
files: [],
|
|
36
|
+
});
|
|
37
|
+
|
|
31
38
|
const invalidateWithActions = (actions: DevChangeAction[]): Extract<BuilderMessage, { type: "invalidate" }> => ({
|
|
32
39
|
type: "invalidate",
|
|
33
40
|
kinds: ["code"],
|
|
@@ -158,8 +165,6 @@ describe("holding requests for a returning builder", () => {
|
|
|
158
165
|
});
|
|
159
166
|
|
|
160
167
|
test("holds through the drain too, not only after the process is gone", () => {
|
|
161
|
-
// The window this decision originally missed: the builder is still alive and refusing, which is
|
|
162
|
-
// the same gap as a restart from anyone waiting on a page.
|
|
163
168
|
expect(shouldHoldForReturningBuilder({ status: "recycling", heldCount: 0 })).toBe(true);
|
|
164
169
|
});
|
|
165
170
|
|
|
@@ -196,8 +201,6 @@ describe("builder rss recycle", () => {
|
|
|
196
201
|
expect(decide({ ceilingBytes: null })).toBe("unbounded");
|
|
197
202
|
});
|
|
198
203
|
|
|
199
|
-
// Rebooting on a generation whose build failed strands the dev server: the replacement hits the
|
|
200
|
-
// same compile error and exits before builder-ready.
|
|
201
204
|
test("defers while the current generation has a failing build", () => {
|
|
202
205
|
expect(decide({ buildFailed: true })).toBe("build-failed");
|
|
203
206
|
});
|
|
@@ -208,34 +211,25 @@ describe("builder rss recycle", () => {
|
|
|
208
211
|
expect(decide({ msSinceLastRecycle: 5_000, minIntervalMs: 1_000 })).toBe("recycle");
|
|
209
212
|
});
|
|
210
213
|
|
|
211
|
-
// Says so, and keeps enforcing: a page load is two route builds, so an app whose builds sit over the
|
|
212
|
-
// ceiling reaches this on its first navigation — which is normal work, not a reason to drop the only
|
|
213
|
-
// bound the builder has.
|
|
214
214
|
test("mentions a tight ceiling rather than acting on it", () => {
|
|
215
215
|
expect(shouldWarnBuilderRssCeilingTight(1)).toBe(false);
|
|
216
216
|
expect(shouldWarnBuilderRssCeilingTight(2)).toBe(true);
|
|
217
217
|
expect(shouldWarnBuilderRssCeilingTight(1, 1)).toBe(true);
|
|
218
218
|
});
|
|
219
219
|
|
|
220
|
-
// The one case recycling cannot fix, measured on the replacement before it has built anything: every
|
|
221
|
-
// future replacement lands on the same floor, so the loop would only ever cost boot builds.
|
|
222
220
|
test("gives up only when a fresh builder is already over the ceiling", () => {
|
|
223
221
|
expect(isRssCeilingUnreachable(ceiling + 1, ceiling)).toBe(true);
|
|
224
222
|
expect(isRssCeilingUnreachable(ceiling - 1, ceiling)).toBe(false);
|
|
225
|
-
// An unreadable rss is no information, and no ceiling is nothing to be unreachable.
|
|
226
223
|
expect(isRssCeilingUnreachable(null, ceiling)).toBe(false);
|
|
227
224
|
expect(isRssCeilingUnreachable(ceiling + 1, null)).toBe(false);
|
|
228
225
|
});
|
|
229
226
|
|
|
230
|
-
// Measured on Linux: the builder peaked at 522MiB and settled at 214MiB with no help, so a 400MiB
|
|
231
|
-
// ceiling recycled a process that was already back under it. The armed sample is always the peak.
|
|
232
227
|
describe("settle check before committing", () => {
|
|
233
228
|
test("waits when the builder is only modestly over the ceiling", () => {
|
|
234
229
|
expect(decideBuilderRssSettle({ rssBytes: 522, ceilingBytes: 400 })).toBe("wait-and-recheck");
|
|
235
230
|
expect(decideBuilderRssSettle({ rssBytes: 401, ceilingBytes: 400 })).toBe("wait-and-recheck");
|
|
236
231
|
});
|
|
237
232
|
|
|
238
|
-
// No purge is going to rescue a builder this far over, so waiting only delays the inevitable.
|
|
239
233
|
test("recycles immediately once far enough past the ceiling", () => {
|
|
240
234
|
expect(decideBuilderRssSettle({ rssBytes: 600, ceilingBytes: 400 })).toBe("recycle-now");
|
|
241
235
|
expect(decideBuilderRssSettle({ rssBytes: 900, ceilingBytes: 400 })).toBe("recycle-now");
|
|
@@ -268,7 +262,6 @@ describe("dev idle suspend", () => {
|
|
|
268
262
|
expect(decide({ restartPending: true })).toBe("restart-pending");
|
|
269
263
|
});
|
|
270
264
|
|
|
271
|
-
// A wake would boot straight back into the same compile error, and the developer is mid-fix anyway.
|
|
272
265
|
test("never suspends on a red build", () => {
|
|
273
266
|
expect(decide({ buildFailed: true })).toBe("build-failed");
|
|
274
267
|
});
|
|
@@ -289,14 +282,13 @@ describe("dev idle suspend", () => {
|
|
|
289
282
|
});
|
|
290
283
|
|
|
291
284
|
test("blocks a suspend on a failure in any phase, not just the newest generation", () => {
|
|
292
|
-
const status = (phase: BuildPhase, ok: boolean): DevBuildStatus => ({ generation: 1, phase, ok, files: [] });
|
|
293
285
|
expect(hasAnyBuildFailure(new Map())).toBe(false);
|
|
294
|
-
expect(hasAnyBuildFailure(new Map([["scan", status("scan", true)]]))).toBe(false);
|
|
286
|
+
expect(hasAnyBuildFailure(new Map([["scan", status("scan", 1, true)]]))).toBe(false);
|
|
295
287
|
expect(
|
|
296
288
|
hasAnyBuildFailure(
|
|
297
289
|
new Map([
|
|
298
|
-
["scan", status("scan", true)],
|
|
299
|
-
["pages", status("pages", false)],
|
|
290
|
+
["scan", status("scan", 1, true)],
|
|
291
|
+
["pages", status("pages", 1, false)],
|
|
300
292
|
]),
|
|
301
293
|
),
|
|
302
294
|
).toBe(true);
|
|
@@ -332,8 +324,6 @@ describe("recycled builder state announcements", () => {
|
|
|
332
324
|
},
|
|
333
325
|
});
|
|
334
326
|
|
|
335
|
-
// Both identities are content hashes, so a recycle with no concurrent edit reproduces them exactly
|
|
336
|
-
// and must not reload the backend — that would refresh every browser on a memory recycle.
|
|
337
327
|
test("suppresses an unchanged pages announcement and relays a moved one", () => {
|
|
338
328
|
expect(shouldRelayRecycledFrontendState(pages("/a/pages-abc.js"), pages("/a/pages-abc.js"))).toBe(false);
|
|
339
329
|
expect(shouldRelayRecycledFrontendState(pages("/a/pages-abc.js"), pages("/a/pages-def.js"))).toBe(true);
|
|
@@ -355,13 +345,6 @@ describe("recycled builder state announcements", () => {
|
|
|
355
345
|
});
|
|
356
346
|
|
|
357
347
|
describe("build status helpers", () => {
|
|
358
|
-
const status = (phase: DevBuildStatus["phase"], generation: number, ok: boolean): DevBuildStatus => ({
|
|
359
|
-
generation,
|
|
360
|
-
phase,
|
|
361
|
-
ok,
|
|
362
|
-
files: [],
|
|
363
|
-
});
|
|
364
|
-
|
|
365
348
|
test("tracks recovery by phase without unrelated phases masking failures", () => {
|
|
366
349
|
const previousByPhase = new Map<DevBuildStatus["phase"], DevBuildStatus>([
|
|
367
350
|
["pages", status("pages", 10, false)],
|
|
@@ -441,13 +424,6 @@ describe("backend recovery abandonment", () => {
|
|
|
441
424
|
});
|
|
442
425
|
|
|
443
426
|
describe("hasBuildFailureForGeneration", () => {
|
|
444
|
-
const status = (phase: DevBuildStatus["phase"], generation: number, ok: boolean): DevBuildStatus => ({
|
|
445
|
-
generation,
|
|
446
|
-
phase,
|
|
447
|
-
ok,
|
|
448
|
-
files: [],
|
|
449
|
-
});
|
|
450
|
-
|
|
451
427
|
test("detects a failing phase recorded for the same generation", () => {
|
|
452
428
|
const statusByPhase = new Map<DevBuildStatus["phase"], DevBuildStatus>([
|
|
453
429
|
["csr", status("csr", 3, false)],
|
package/akanApp/devHostPolicy.ts
CHANGED
|
@@ -11,11 +11,7 @@ import type { IncrementalBuilderStatus } from "../incrementalBuilder";
|
|
|
11
11
|
|
|
12
12
|
const BACKEND_RECOVERY_MAX_ATTEMPTS = 5;
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
* How many requests may wait for a builder that is coming back. Generous — a page load asks for
|
|
16
|
-
* several routes — but finite, so a builder that never returns cannot grow this without bound. Past
|
|
17
|
-
* it, requests are failed as they were before, which is the behaviour this limit falls back to.
|
|
18
|
-
*/
|
|
14
|
+
// A page load asks for several routes, but a builder that never returns must not grow the hold queue unbounded.
|
|
19
15
|
const HELD_BUILDER_REQUEST_LIMIT = 64;
|
|
20
16
|
|
|
21
17
|
export const BUILDER_MIN_RSS_RECYCLE_INTERVAL_MS = 30_000;
|
|
@@ -25,8 +21,6 @@ const BUILDER_TIGHT_RSS_REPORT_LIMIT = 2;
|
|
|
25
21
|
// Far enough above the ceiling that no purge would rescue it; recycle without waiting.
|
|
26
22
|
const BUILDER_RSS_HARD_MULTIPLE = 1.5;
|
|
27
23
|
|
|
28
|
-
// A sandbox between user turns pays for a watcher that is watching nothing change. Suspending build
|
|
29
|
-
// capacity after this long returns the builder's residency until the next edit or route request.
|
|
30
24
|
const DEV_IDLE_SUSPEND_MS = 300_000;
|
|
31
25
|
|
|
32
26
|
// A wake that immediately suspends again would flap around whatever woke it.
|
|
@@ -34,7 +28,6 @@ const DEV_IDLE_MIN_UPTIME_MS = 30_000;
|
|
|
34
28
|
|
|
35
29
|
export const SOURCE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]);
|
|
36
30
|
|
|
37
|
-
/\.(css|scss|sass|less|json|svg|png|jpe?g|webp|gif|avif|ico|woff2?|ttf|otf|mp3|mp4|wav|html)$/i;
|
|
38
31
|
const SERVER_SUFFIXES = [".service.ts", ".document.ts"];
|
|
39
32
|
|
|
40
33
|
const SHARED_SUFFIXES = [".constant.ts", ".dictionary.ts", ".signal.ts"];
|
|
@@ -53,10 +46,6 @@ export const shouldRestartBackendByDevPlan = (
|
|
|
53
46
|
export const shouldRestartBuilderByDevPlan = (message: Extract<BuilderMessage, { type: "invalidate" }>): boolean =>
|
|
54
47
|
message.devPlan?.actions.includes("restart-builder") ?? false;
|
|
55
48
|
|
|
56
|
-
/**
|
|
57
|
-
* A backend that keeps dying isn't going to heal by retrying the same code; after this many
|
|
58
|
-
* consecutive attempts the host idles and the next server-side edit triggers a fresh restart.
|
|
59
|
-
*/
|
|
60
49
|
export const shouldAbandonBackendRecovery = (attempts: number, maxAttempts = BACKEND_RECOVERY_MAX_ATTEMPTS): boolean =>
|
|
61
50
|
attempts >= maxAttempts;
|
|
62
51
|
|
|
@@ -69,11 +58,7 @@ export const shouldRestartDevHostByDevPlan = (message: Extract<BuilderMessage, {
|
|
|
69
58
|
|
|
70
59
|
export type BackendLifecycleState = "starting" | "ready" | "restart-pending" | "stopping" | "recovering" | "stopped";
|
|
71
60
|
|
|
72
|
-
/**
|
|
73
|
-
* What a dev host tells a supervising process about itself. Reported rather than scraped: the parent
|
|
74
|
-
* gets one pipe of interleaved child output, in which "this app is up" is indistinguishable from a line
|
|
75
|
-
* that merely mentions it.
|
|
76
|
-
*/
|
|
61
|
+
/** Reported, not scraped: in interleaved child output "this app is up" reads like any line mentioning it. */
|
|
77
62
|
export type DevHostState = "starting" | "ready" | "restarting" | "recovering" | "suspended" | "failed" | "stopped";
|
|
78
63
|
|
|
79
64
|
export interface DevHostEvent {
|
|
@@ -91,11 +76,7 @@ const devHostStateByBackendState = {
|
|
|
91
76
|
stopped: "stopped",
|
|
92
77
|
} as const satisfies { [key in BackendLifecycleState]: DevHostState };
|
|
93
78
|
|
|
94
|
-
/**
|
|
95
|
-
* The backend's own lifecycle collapsed to what a supervisor can act on. `stopped` is the one state
|
|
96
|
-
* that is ambiguous on its own — a planned shutdown and a crash loop that ran out of retries both land
|
|
97
|
-
* there — so the give-up flag is what separates "gone" from "broken".
|
|
98
|
-
*/
|
|
79
|
+
/** `stopped` is both a planned shutdown and an exhausted crash loop; `gaveUp` maps the latter to `failed`. */
|
|
99
80
|
export const devHostStateOf = (state: BackendLifecycleState, gaveUp: boolean): DevHostState =>
|
|
100
81
|
state === "stopped" && gaveUp ? "failed" : devHostStateByBackendState[state];
|
|
101
82
|
|
|
@@ -192,12 +173,7 @@ export const shouldReplaceLastGoodMessage = (
|
|
|
192
173
|
export const shouldQueueBuildStatusReplay = (backendReady: boolean, pendingReplayCount: number): boolean =>
|
|
193
174
|
!backendReady || pendingReplayCount > 0;
|
|
194
175
|
|
|
195
|
-
/**
|
|
196
|
-
* Recycling the builder/backend on a generation whose build already failed is guaranteed to strand
|
|
197
|
-
* the dev server: the rebooted builder hits the same compile error and exits before builder-ready.
|
|
198
|
-
* Failing phase statuses for a generation arrive over IPC before that generation's invalidate, so
|
|
199
|
-
* the host can check them here and defer the recycle until a healthy batch lands.
|
|
200
|
-
*/
|
|
176
|
+
/** Failing statuses arrive before their generation's invalidate; recycling on one strands the dev server. */
|
|
201
177
|
export const hasBuildFailureForGeneration = (
|
|
202
178
|
statusByPhase: ReadonlyMap<BuildPhase, DevBuildStatus>,
|
|
203
179
|
generation: number | undefined,
|
|
@@ -211,15 +187,7 @@ export const hasBuildFailureForGeneration = (
|
|
|
211
187
|
|
|
212
188
|
export type BuilderRssRecycleDecision = "unbounded" | "below-ceiling" | "build-failed" | "too-soon" | "recycle";
|
|
213
189
|
|
|
214
|
-
/**
|
|
215
|
-
* Whether an over-ceiling builder should be replaced now.
|
|
216
|
-
*
|
|
217
|
-
* `Bun.build` never returns its native arenas, so the builder's RSS only comes back when the process
|
|
218
|
-
* exits. Recycling it is therefore the only bound available — but it costs a boot build, so the two
|
|
219
|
-
* cases where a recycle cannot help are excluded: a generation whose build already failed (the
|
|
220
|
-
* replacement would hit the same compile error), and a recycle so soon after the last one that the
|
|
221
|
-
* ceiling is evidently unreachable for this app.
|
|
222
|
-
*/
|
|
190
|
+
/** `Bun.build` never returns its native arenas, so recycling the builder is the only bound on its RSS. */
|
|
223
191
|
export const decideBuilderRssRecycle = ({
|
|
224
192
|
rssBytes,
|
|
225
193
|
ceilingBytes,
|
|
@@ -242,11 +210,6 @@ export const decideBuilderRssRecycle = ({
|
|
|
242
210
|
|
|
243
211
|
export type BuilderRssSettleDecision = "recycle-now" | "wait-and-recheck";
|
|
244
212
|
|
|
245
|
-
/**
|
|
246
|
-
* Whether an armed recycle should wait out the allocator's purge window before committing. A builder
|
|
247
|
-
* far enough over the ceiling is not going to be rescued by a purge, so waiting there only delays a
|
|
248
|
-
* recycle that has to happen.
|
|
249
|
-
*/
|
|
250
213
|
export const decideBuilderRssSettle = ({
|
|
251
214
|
rssBytes,
|
|
252
215
|
ceilingBytes,
|
|
@@ -267,15 +230,7 @@ export type IdleSuspendDecision =
|
|
|
267
230
|
| "too-soon"
|
|
268
231
|
| "suspend";
|
|
269
232
|
|
|
270
|
-
/**
|
|
271
|
-
* Whether the dev host may drop its build capacity now. Every "no" here is a case where suspending
|
|
272
|
-
* would either lose work or produce a wake that immediately re-suspends:
|
|
273
|
-
*
|
|
274
|
-
* - a red build means the developer is mid-fix and about to save again, and a wake would boot straight
|
|
275
|
-
* back into the same error via the degraded-boot path
|
|
276
|
-
* - a pending restart/recovery already has its own plan for the builder
|
|
277
|
-
* - `too-soon` keeps a wake from flapping around whatever triggered it
|
|
278
|
-
*/
|
|
233
|
+
/** A red build means a save is imminent, and a wake would boot straight back into the same error. */
|
|
279
234
|
export const decideIdleSuspend = ({
|
|
280
235
|
enabled,
|
|
281
236
|
suspended,
|
|
@@ -313,28 +268,13 @@ export const resolveIdleSuspendMs = (raw: string | undefined): number | null =>
|
|
|
313
268
|
return Math.round(parsed);
|
|
314
269
|
};
|
|
315
270
|
|
|
316
|
-
/** Any red phase blocks a suspend, unlike the rss recycle which only cares about one generation. */
|
|
317
271
|
export const hasAnyBuildFailure = (statusByPhase: ReadonlyMap<BuildPhase, DevBuildStatus>): boolean =>
|
|
318
272
|
[...statusByPhase.values()].some((status) => !status.ok);
|
|
319
273
|
|
|
320
|
-
/**
|
|
321
|
-
* A config change while suspended cannot be applied by restarting the builder alone — the dev host
|
|
322
|
-
* itself has to re-read the config, which is the same path an ordinary config save takes.
|
|
323
|
-
*/
|
|
324
274
|
export const shouldRefreshConfigOnIdleWake = (batch: ChangeBatch | null): boolean =>
|
|
325
275
|
!!batch && batch.kinds.has("config");
|
|
326
276
|
|
|
327
|
-
|
|
328
|
-
* Whether a request that arrived while the builder was away should wait for the one coming back.
|
|
329
|
-
*
|
|
330
|
-
* A recycle or a crash-restart is a gap, not a failure — the request that lands in it is the page a
|
|
331
|
-
* developer is waiting on. A stopped builder is a different thing: nothing is bringing it back, so
|
|
332
|
-
* waiting would only delay the error.
|
|
333
|
-
*
|
|
334
|
-
* `recycling` is in here for the same reason as `restarting`, and was the hole this decision shipped
|
|
335
|
-
* with: a draining builder is still alive, so the request reached it and came back refused while the
|
|
336
|
-
* host still thought there was nothing to wait for.
|
|
337
|
-
*/
|
|
277
|
+
// `recycling` too: a draining builder is alive and refuses; nothing brings a `stopped` one back, so it fails fast.
|
|
338
278
|
const RETURNING_BUILDER_STATUSES = new Set<IncrementalBuilderStatus>(["starting", "recycling", "restarting"]);
|
|
339
279
|
export const shouldHoldForReturningBuilder = ({
|
|
340
280
|
status,
|
|
@@ -346,37 +286,17 @@ export const shouldHoldForReturningBuilder = ({
|
|
|
346
286
|
limit?: number;
|
|
347
287
|
}): boolean => RETURNING_BUILDER_STATUSES.has(status) && heldCount < limit;
|
|
348
288
|
|
|
349
|
-
/**
|
|
350
|
-
* Whether a builder that is over the ceiling again this soon after being replaced is worth saying so
|
|
351
|
-
* about, once. Not a reason to stop enforcing the ceiling: the minimum interval already bounds what
|
|
352
|
-
* this costs at one recycle per interval, and dropping the bound is how a container gets OOM-killed.
|
|
353
|
-
*
|
|
354
|
-
* This used to disable the ceiling for the session, on a count that a single page load reaches — two
|
|
355
|
-
* route builds, two reports, both inside the interval. That is normal work on any app whose builds sit
|
|
356
|
-
* above the ceiling, which is the same app the ceiling was derived for.
|
|
357
|
-
*/
|
|
289
|
+
/** Warn only, never stop enforcing: dropping the ceiling is how a container gets OOM-killed. */
|
|
358
290
|
export const shouldWarnBuilderRssCeilingTight = (
|
|
359
291
|
reportsSinceRecycle: number,
|
|
360
292
|
limit = BUILDER_TIGHT_RSS_REPORT_LIMIT,
|
|
361
293
|
): boolean => reportsSinceRecycle >= limit;
|
|
362
294
|
|
|
363
|
-
/**
|
|
364
|
-
* Whether recycling can ever bring this builder under the ceiling.
|
|
365
|
-
*
|
|
366
|
-
* Measured on a replacement the moment it is ready, before it has built anything on demand: that is
|
|
367
|
-
* the floor every future replacement lands on, so a floor already over the ceiling is the one case
|
|
368
|
-
* where the recycle loop is pure cost. It is also the case the escape hatch was always described as
|
|
369
|
-
* being for — the previous rule inferred it from report timing and caught ordinary work instead.
|
|
370
|
-
*/
|
|
295
|
+
/** Takes a fresh replacement's RSS: the floor every future replacement lands on. */
|
|
371
296
|
export const isRssCeilingUnreachable = (freshRssBytes: number | null, ceilingBytes: number | null): boolean =>
|
|
372
297
|
freshRssBytes !== null && ceilingBytes !== null && freshRssBytes >= ceilingBytes;
|
|
373
298
|
|
|
374
|
-
/**
|
|
375
|
-
* Whether a recycled builder's re-announced boot artifact actually differs from what the backend
|
|
376
|
-
* already has. Both payload identities are content hashes — `pages-[hash].js` and
|
|
377
|
-
* `<name>-[hash].css` — so an unchanged recycle produces identical ones and needs no reload. Only a
|
|
378
|
-
* save that raced the recycle moves them, and that is the case worth pushing.
|
|
379
|
-
*/
|
|
299
|
+
/** Both identities are content hashes (`pages-[hash].js`, `<name>-[hash].css`), unchanged by a clean recycle. */
|
|
380
300
|
export const shouldRelayRecycledFrontendState = (
|
|
381
301
|
current:
|
|
382
302
|
| Extract<BuilderMessage, { type: "pages-updated" }>
|
|
@@ -409,7 +329,6 @@ const mergeDevPlans = (current?: DevChangePlan, next?: DevChangePlan): DevChange
|
|
|
409
329
|
};
|
|
410
330
|
};
|
|
411
331
|
|
|
412
|
-
/** A deferred recycle accumulates every batch it skipped so the eventual restart covers them all. */
|
|
413
332
|
export const mergeInvalidateMessages = (
|
|
414
333
|
current: Extract<BuilderMessage, { type: "invalidate" }>,
|
|
415
334
|
next: Extract<BuilderMessage, { type: "invalidate" }>,
|
|
@@ -429,14 +348,9 @@ export const buildStatusReplaySequence = (
|
|
|
429
348
|
latestByPhase: ReadonlyMap<BuildPhase, DevBuildStatus>,
|
|
430
349
|
): DevBuildStatus[] => [...pendingReplay, ...latestByPhase.values()];
|
|
431
350
|
|
|
432
|
-
/** `
|
|
351
|
+
/** `mtimeMs:size` per file. */
|
|
433
352
|
export type SourceFingerprints = ReadonlyMap<string, string>;
|
|
434
353
|
|
|
435
|
-
/**
|
|
436
|
-
* Which of the files in `before` are no longer stamped the way they were.
|
|
437
|
-
*
|
|
438
|
-
* Only files present in `before` are compared. The question this answers is which *running* code went
|
|
439
|
-
* stale while nothing was watching, and a file that did not exist then is not running anywhere.
|
|
440
|
-
*/
|
|
354
|
+
/** Only files in `before`: a file that did not exist then is not running anywhere. */
|
|
441
355
|
export const filesChangedSince = (before: SourceFingerprints, after: SourceFingerprints): string[] =>
|
|
442
356
|
[...before].filter(([file, stamp]) => after.get(file) !== stamp).map(([file]) => file);
|
|
@@ -33,6 +33,22 @@ const baseDevEnv = {
|
|
|
33
33
|
workspaceRoot: "/workspace",
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
const loadExtAppConfig = async (tmpPrefix: string, appConfig: string, libConfig: string) => {
|
|
37
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), tmpPrefix));
|
|
38
|
+
try {
|
|
39
|
+
fs.mkdirSync(path.join(root, "apps/extapp"), { recursive: true });
|
|
40
|
+
fs.mkdirSync(path.join(root, "libs/extlib"), { recursive: true });
|
|
41
|
+
fs.writeFileSync(path.join(root, "apps/extapp/akan.config.ts"), appConfig);
|
|
42
|
+
fs.writeFileSync(path.join(root, "libs/extlib/akan.config.ts"), libConfig);
|
|
43
|
+
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "extrepo", version: "1.0.0" }));
|
|
44
|
+
fs.writeFileSync(path.join(root, ".env"), "AKAN_PUBLIC_REPO_NAME=extrepo\nAKAN_PUBLIC_SERVE_DOMAIN=ext.test\n");
|
|
45
|
+
const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "extrepo" });
|
|
46
|
+
return await AppExecutor.from(workspace, "extapp").getConfig();
|
|
47
|
+
} finally {
|
|
48
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
36
52
|
describe("AkanAppConfig", () => {
|
|
37
53
|
test("applies defaults for route domains, i18n, image, mobile, and imports", () => {
|
|
38
54
|
const config = new AkanAppConfig(app, ["shared"], packageJson, {}, baseDevEnv);
|
|
@@ -340,8 +356,7 @@ describe("AkanAppConfig", () => {
|
|
|
340
356
|
expect(config.getMissingDatabaseModeDependencySpecs("cluster")).toEqual([
|
|
341
357
|
`postgres@${runtimeDependencies.postgres}`,
|
|
342
358
|
]);
|
|
343
|
-
// The workspace-root install covers the toolchain
|
|
344
|
-
// (deduped — "@capacitor/core" appears in both source lists).
|
|
359
|
+
// The workspace-root install covers the toolchain plus every app Capacitor plugin, deduped (`@capacitor/core`).
|
|
345
360
|
const expectedMobilePackages: string[] = [
|
|
346
361
|
"@capacitor/cli",
|
|
347
362
|
"@capacitor/core",
|
|
@@ -491,25 +506,13 @@ describe("AkanAppConfig lib externalLibs", () => {
|
|
|
491
506
|
});
|
|
492
507
|
|
|
493
508
|
test("reads them off every workspace lib config on load", async () => {
|
|
494
|
-
const
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
fs.writeFileSync(
|
|
500
|
-
path.join(root, "libs/extlib/akan.config.ts"),
|
|
501
|
-
"export default { externalLibs: ['puppeteer'] };\n",
|
|
502
|
-
);
|
|
503
|
-
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "extrepo", version: "1.0.0" }));
|
|
504
|
-
fs.writeFileSync(path.join(root, ".env"), "AKAN_PUBLIC_REPO_NAME=extrepo\nAKAN_PUBLIC_SERVE_DOMAIN=ext.test\n");
|
|
505
|
-
|
|
506
|
-
const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "extrepo" });
|
|
507
|
-
const config = await AppExecutor.from(workspace, "extapp").getConfig();
|
|
509
|
+
const config = await loadExtAppConfig(
|
|
510
|
+
"akan-config-libext-",
|
|
511
|
+
"export default { externalLibs: ['shiki'] };\n",
|
|
512
|
+
"export default { externalLibs: ['puppeteer'] };\n",
|
|
513
|
+
);
|
|
508
514
|
|
|
509
|
-
|
|
510
|
-
} finally {
|
|
511
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
512
|
-
}
|
|
515
|
+
expect(config.externalLibs).toEqual(["shiki", "puppeteer"]);
|
|
513
516
|
});
|
|
514
517
|
});
|
|
515
518
|
|
|
@@ -553,26 +556,14 @@ describe("AkanAppConfig lib docker runs", () => {
|
|
|
553
556
|
});
|
|
554
557
|
|
|
555
558
|
test("reads them off every workspace lib config on load", async () => {
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
fs.writeFileSync(
|
|
562
|
-
path.join(root, "libs/extlib/akan.config.ts"),
|
|
563
|
-
"export default { docker: { preRuns: ['echo from-lib'], postRuns: [{ arm64: 'echo arm-only' }] } };\n",
|
|
564
|
-
);
|
|
565
|
-
fs.writeFileSync(path.join(root, "package.json"), JSON.stringify({ name: "extrepo", version: "1.0.0" }));
|
|
566
|
-
fs.writeFileSync(path.join(root, ".env"), "AKAN_PUBLIC_REPO_NAME=extrepo\nAKAN_PUBLIC_SERVE_DOMAIN=ext.test\n");
|
|
567
|
-
|
|
568
|
-
const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "extrepo" });
|
|
569
|
-
const config = await AppExecutor.from(workspace, "extapp").getConfig();
|
|
559
|
+
const config = await loadExtAppConfig(
|
|
560
|
+
"akan-config-libdocker-",
|
|
561
|
+
"export default {};\n",
|
|
562
|
+
"export default { docker: { preRuns: ['echo from-lib'], postRuns: [{ arm64: 'echo arm-only' }] } };\n",
|
|
563
|
+
);
|
|
570
564
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
} finally {
|
|
574
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
575
|
-
}
|
|
565
|
+
expect(config.dockerfile).toContain("RUN echo from-lib");
|
|
566
|
+
expect(config.dockerfile).toContain('RUN if [ "$TARGETARCH" = "arm64"');
|
|
576
567
|
});
|
|
577
568
|
});
|
|
578
569
|
|