@akanjs/devkit 3.0.0-beta.3 → 3.0.0-beta.31
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/DEV_RUNTIME_KNOBS.md +44 -26
- package/README.ko.md +2 -2
- package/README.md +3 -2
- 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 +56 -3
- package/akanApp/akanApp.host.ts +218 -278
- package/akanApp/devBootLatch.test.ts +39 -0
- package/akanApp/devBootLatch.ts +39 -0
- package/akanApp/devHostPolicy.test.ts +81 -35
- package/akanApp/devHostPolicy.ts +41 -102
- package/akanConfig/akanBin.test.ts +62 -0
- package/akanConfig/akanBin.ts +78 -0
- package/akanConfig/akanConfig.test.ts +427 -141
- package/akanConfig/akanConfig.ts +327 -275
- package/akanConfig/index.ts +1 -0
- package/akanConfig/types.ts +21 -8
- package/akanContext.ts +110 -160
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.test.ts +41 -5
- package/applicationBuildRunner.ts +71 -63
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +32 -30
- package/artifact/implicitRootLayout.ts +58 -106
- package/artifact/routeSeedIndex.test.ts +7 -6
- package/artifact/routeSeedIndex.ts +11 -44
- package/biome.base.json +35 -2
- package/biomeBase.ts +2 -5
- 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 +71 -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/csrE2e/csrE2eHarness.fixture.ts +505 -0
- package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
- package/csrE2e/csrPageStack.e2e.test.ts +252 -0
- package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
- package/csrE2e/csrRootLayout.e2e.test.ts +134 -0
- package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
- package/csrE2e/registryVendor.e2e.test.ts +221 -0
- package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
- 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 +176 -106
- package/executors.ts +212 -498
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +5 -3
- package/fileSys.ts +22 -9
- 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 +123 -37
- package/frontendBuild/bundleDefine.test.ts +12 -0
- package/frontendBuild/bundleDefine.ts +16 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.test.ts +112 -0
- package/frontendBuild/clientEntryDiscovery.ts +122 -110
- package/frontendBuild/csrArtifactBuilder.ts +9 -31
- package/frontendBuild/csrDevArtifactWriter.test.ts +151 -0
- package/frontendBuild/csrDevArtifactWriter.ts +307 -0
- package/frontendBuild/csrDevBundler.test.ts +66 -0
- package/frontendBuild/csrDevBundler.ts +95 -0
- package/frontendBuild/csrDevFactoryCheck.test.ts +33 -0
- package/frontendBuild/csrDevFactoryCheck.ts +39 -0
- package/frontendBuild/csrDevModuleCompiler.test.ts +293 -0
- package/frontendBuild/csrDevModuleCompiler.ts +422 -0
- package/frontendBuild/csrDevPatcher.test.ts +572 -0
- package/frontendBuild/csrDevPatcher.ts +548 -0
- package/frontendBuild/csrDevPaths.ts +145 -0
- package/frontendBuild/csrDevResolver.test.ts +239 -0
- package/frontendBuild/csrDevResolver.ts +363 -0
- package/frontendBuild/csrDevTypes.ts +61 -0
- package/frontendBuild/csrEntryFiles.ts +24 -0
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +31 -82
- 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/devRegistryBundler.ts +192 -0
- 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 +57 -70
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +59 -32
- package/frontendBuild/hmrWatcher.ts +31 -74
- package/frontendBuild/index.ts +4 -0
- package/frontendBuild/pagesBundleBuilder.ts +28 -30
- package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
- package/frontendBuild/registryMetadataFingerprint.ts +28 -0
- package/frontendBuild/routeClientBuilder.ts +100 -62
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/serverGraphFile.test.ts +77 -0
- package/frontendBuild/serverGraphFile.ts +93 -0
- package/frontendBuild/sourceMtimeIndex.test.ts +41 -97
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +19 -13
- package/frontendBuild/ssrDevBundler.ts +85 -0
- 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 +147 -63
- package/incrementalBuilder/buildBatchProtocol.ts +13 -38
- package/incrementalBuilder/buildBatchRunner.test.ts +65 -0
- package/incrementalBuilder/buildBatchRunner.ts +26 -38
- 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/builderWorkQueue.test.ts +94 -0
- package/incrementalBuilder/builderWorkQueue.ts +126 -0
- package/incrementalBuilder/devWatchBatch.test.ts +21 -24
- package/incrementalBuilder/incrementalBuilder.host.test.ts +53 -56
- package/incrementalBuilder/incrementalBuilder.host.ts +68 -81
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +269 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +596 -208
- package/index.ts +5 -19
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +105 -199
- 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/__fixtures__/no-document-cookie/bad.tsx +4 -0
- package/lint/__fixtures__/no-document-cookie/good.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/bad.tsx +7 -0
- package/lint/__fixtures__/no-web-only-api-outside-webkit/good.tsx +11 -0
- package/lint/__fixtures__/no-web-storage/bad.tsx +6 -0
- package/lint/__fixtures__/no-web-storage/good.tsx +7 -0
- package/lint/gritRules.test.ts +8 -24
- package/lint/no-deep-internal-import.grit +2 -2
- package/lint/no-document-cookie.grit +29 -0
- package/lint/no-redeclare-predefined-endpoint.grit +2 -1
- package/lint/no-web-only-api-outside-webkit.grit +44 -0
- package/lint/no-web-storage.grit +30 -0
- package/linter.test.ts +1 -2
- package/linter.ts +26 -116
- package/mobile/desktopBin.test.ts +177 -0
- package/mobile/desktopBin.ts +143 -0
- package/mobile/desktopServerStage.test.ts +204 -0
- package/mobile/desktopServerStage.ts +138 -0
- package/mobile/index.ts +9 -0
- package/mobile/mobileTarget.test.ts +57 -8
- package/mobile/mobileTarget.ts +45 -26
- package/mobile/nativeApi.test.ts +30 -0
- package/mobile/nativeApi.ts +40 -0
- package/mobile/nativeApp.test.ts +177 -0
- package/mobile/nativeApp.ts +416 -0
- package/mobile/nativeAppLine.test.ts +69 -0
- package/mobile/nativeAppLine.ts +71 -0
- package/mobile/nativeConfig.test.ts +414 -0
- package/mobile/nativeConfig.ts +293 -0
- package/mobile/nativePluginFolders.test.ts +81 -0
- package/mobile/nativePluginFolders.ts +60 -0
- package/mobile/nativeWebDir.test.ts +80 -0
- package/mobile/nativeWebDir.ts +66 -0
- package/mobile/placeholderAppId.test.ts +13 -0
- package/mobile/placeholderAppId.ts +16 -0
- package/mobile/usageDescriptions.test.ts +27 -0
- package/mobile/usageDescriptions.ts +24 -0
- package/package.json +11 -11
- 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 +200 -286
- 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 +91 -225
- 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 +42 -20
- package/transforms/serverEnvPlugin.test.ts +103 -0
- package/transforms/serverEnvPlugin.ts +41 -0
- package/transforms/transforms.test.ts +166 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +6 -23
- package/typeChecker.ts +7 -63
- package/types.ts +2 -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.test.ts +18 -3
- package/workspaceLayout.ts +22 -23
- package/abstractCompactor.ts +0 -112
- package/aiEditor.test.ts +0 -60
- package/aiEditor.ts +0 -387
- package/applicationReleasePackager.ts +0 -209
- package/capacitorApp.test.ts +0 -372
- package/capacitorApp.ts +0 -1648
- 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
package/DEV_RUNTIME_KNOBS.md
CHANGED
|
@@ -22,7 +22,7 @@ crosses its ceiling is replaced when it is next idle — never mid-work.
|
|
|
22
22
|
|
|
23
23
|
| process | explicit (MiB) | explicit (bytes) | share of the limit | fallback with no limit |
|
|
24
24
|
|---|---|---|---|---|
|
|
25
|
-
| incremental builder | `AKAN_BUILDER_MAX_RSS_MB` | `AKAN_BUILDER_MAX_RSS` | **0.35** |
|
|
25
|
+
| incremental builder | `AKAN_BUILDER_MAX_RSS_MB` | `AKAN_BUILDER_MAX_RSS` | **0.35** | 2048MB (dev) |
|
|
26
26
|
| RSC worker | `AKAN_RSC_WORKER_MAX_RSS_MB` | `AKAN_RSC_WORKER_MAX_RSS` | **0.55** | 768MB (dev), unbounded (production) |
|
|
27
27
|
|
|
28
28
|
`AKAN_BUILDER_MAX_RSS_MB=0` leaves the builder unbounded, which is the escape hatch for an app whose
|
|
@@ -37,8 +37,10 @@ this repo's own `apps/akan`). None of those has a ceiling, and none is subtracte
|
|
|
37
37
|
|
|
38
38
|
In practice this holds because the two ceilings are rarely at their limit simultaneously and the build
|
|
39
39
|
worker exits. But if you are sizing a container to a hard number, size it against the *sum of observed
|
|
40
|
-
peaks*, not against these fractions. Measured
|
|
41
|
-
~
|
|
40
|
+
peaks*, not against these fractions. Measured on `apps/minimal` (macOS, 2026-09-29), for reference: the
|
|
41
|
+
builder ~120MB at rest after boot, ~540MB right after its first route build (the bundler hands most of it
|
|
42
|
+
back after ~20s idle), ~250MB while it patches saves into the SSR registry; the RSC worker ~75MB after boot
|
|
43
|
+
and ~110MB after route builds; the build worker ~900MB for the SSR registry's boot build.
|
|
42
44
|
|
|
43
45
|
## Recycling behaviour
|
|
44
46
|
|
|
@@ -65,10 +67,17 @@ which is the one case where replacing it again cannot help; it says that too, an
|
|
|
65
67
|
|
|
66
68
|
| variable | default | what it does |
|
|
67
69
|
|---|---|---|
|
|
68
|
-
| `AKAN_DEV_CSR_REBUILD` | off | Rebuild the CSR artifact on every save. Armed automatically by the first `/__csr` or `?csr=true` request, which is what a mobile WebView session does — set it explicitly only to have it from boot. |
|
|
70
|
+
| `AKAN_DEV_CSR_REBUILD` | off | Rebuild the CSR artifact on every save. Armed automatically by the first `/__csr` or `?csr=true` request, which is what a mobile WebView session does — set it explicitly only to have it from boot. Set at boot, its CSR build runs beside the SSR registry's: on apps/akan the builder and its workers peak at about 2.7GB instead of 1.3GB, and the next boot wave waits for both builds. |
|
|
71
|
+
| `AKAN_DEV_CSR` | `registry` | How the dev CSR page is built. `registry` serves it as a module registry that patches the modules a save changed; `artifact` brings back the single-file bundle, which reloads on every save. |
|
|
72
|
+
| `AKAN_DEV_CSR_PATCHER` | on | `off` sends every save of a dev module registry — the CSR page's, and the one SSR pages load their client code from (`.akan/artifact/ssr-dev`) — to a disposable build worker instead of patching it in the resident builder. The patcher keeps each registry in memory (on minimal the CSR one adds about 170MB to the builder's median RSS and 210MB to its peak, flat across saves) and hands whole-app builds to a worker: a first build, a new npm module, a bare import no file of its package resolved before, a config change, a signal or dictionary save (their metadata is inlined at build time), and a registry whose last whole build was cut short. A misspelled import fails in the builder, like a missing file. A builder that dies mid-patch turns it off until the dev server next replaces its builder and backend together (a config, signal or dictionary change). |
|
|
73
|
+
| `AKAN_DEV_WATCH_DEBOUNCE_MS` | `30` | How long the file watcher collects changes before it hands a batch over. Every save waits this long before anything builds. A second write inside the window joins the same batch; one after it starts another batch, which reads the file's latest content, so a format-on-save costs a second build rather than a wrong one. |
|
|
69
74
|
| `AKAN_BUILDER_RPC_TIMEOUT_MS` | `120000` | How long the backend waits for a builder answer. Generous on purpose: a cold CSR build of every page legitimately takes tens of seconds. |
|
|
70
75
|
| `AKAN_SERVER_PAGES_SPLITTING` | off | Emit the server pages bundle as chunks instead of one file. Experimental — the memory/latency trade has not been measured on a real app. |
|
|
71
76
|
|
|
77
|
+
Two more are read by the dev server for its end-to-end tests only; never set them. `AKAN_DEV_SSR_ARM_DELAY_MS`
|
|
78
|
+
holds the SSR registry's boot build back, and `AKAN_CSR_DEV_APP_WRITE_DELAY_MS` holds a registry's `app.js`
|
|
79
|
+
back after a patch is announced.
|
|
80
|
+
|
|
72
81
|
## Observability
|
|
73
82
|
|
|
74
83
|
| variable | default | what it does |
|
|
@@ -80,31 +89,36 @@ which is the one case where replacing it again cannot help; it says that too, an
|
|
|
80
89
|
## Several apps at once
|
|
81
90
|
|
|
82
91
|
`akan start a,b` runs one dev host per app under a supervisor, so **every number above multiplies by the number
|
|
83
|
-
of apps** — there is no shared builder and no shared RSC worker. Measured on this repo
|
|
92
|
+
of apps** — there is no shared builder and no shared RSC worker. Measured on this repo 8 seconds after both apps
|
|
84
93
|
finished booting:
|
|
85
94
|
|
|
86
95
|
| process | akan | minimal |
|
|
87
96
|
|---|---|---|
|
|
88
|
-
| dev host |
|
|
89
|
-
| incremental builder |
|
|
90
|
-
| backend |
|
|
91
|
-
| RSC worker |
|
|
92
|
-
|
|
93
|
-
Plus ~
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
+
| dev host | 279MB | 196MB |
|
|
98
|
+
| incremental builder | 182MB | 114MB |
|
|
99
|
+
| backend (gateway and replica) | 123MB | 130MB |
|
|
100
|
+
| RSC worker | 162MB | 75MB |
|
|
101
|
+
|
|
102
|
+
Plus ~75MB for the supervisor itself: **~1.3GB for two apps at rest.** The peak is the boot, not the rest: the
|
|
103
|
+
build workers — the base build, then the SSR registry's — take a whole akan dev host to ~1.9GB and minimal's to
|
|
104
|
+
~1.4GB, and booted one after the other the two peaked at 2.2GB. The builders are also the part that goes away —
|
|
105
|
+
`AKAN_DEV_IDLE_SUSPEND_MS` releases each one independently, so a session where you are editing one app keeps one
|
|
106
|
+
builder, and every idle app keeps only its dev host, backend and RSC worker. **Multi-app is sized against idle
|
|
107
|
+
suspend being on**; setting it to `0` keeps every builder resident for the whole session.
|
|
97
108
|
|
|
98
109
|
Two things bound the peak rather than the floor:
|
|
99
110
|
|
|
100
|
-
- **`--concurrency` (default: what the machine allows).** Apps boot in waves
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
- **`--concurrency` (default: what the machine allows).** Apps boot in waves. The next wave starts once each
|
|
112
|
+
app of the previous one serves and its builder's boot builds have settled — the SSR registry's, and CSR's when
|
|
113
|
+
`AKAN_DEV_CSR_REBUILD=1` arms it — since those workers are the largest processes of a boot. An app that never
|
|
114
|
+
reports them starts the next wave 30 seconds after it serves. It shows as ready, and `--open` opens it, as
|
|
115
|
+
soon as it serves. Booting `n` apps at once means `n` overlapping peaks — which is what OOM-kills a container
|
|
116
|
+
that would have been fine with them staggered. Unset, the wave is
|
|
117
|
+
`min(apps, half the memory budget / 1.8GB per app, cores / 4)`, never below one, and the session prints
|
|
118
|
+
which — so a laptop boots its apps together and a small container boots them one at a time. The 1.8GB is
|
|
119
|
+
apps/akan's boot peak across all its processes (measured 1.73–1.9GB). The memory budget is the smaller of
|
|
120
|
+
the host's RAM and `AKAN_MEMORY_LIMIT` / the cgroup limit; `os.freemem()` is not consulted, because it counts
|
|
121
|
+
free pages rather than reclaimable ones and reports ~0.3GB on an idle 48GB laptop.
|
|
108
122
|
- **`AKAN_MEMORY_LIMIT` is per process, not per session.** Each dev host derives its builder and RSC-worker
|
|
109
123
|
ceilings from it independently, so a limit sized for one app does not become a budget for four. Divide it
|
|
110
124
|
yourself, or leave it unset on a laptop. The one place it is read as a session ceiling is the boot wave
|
|
@@ -113,18 +127,22 @@ Two things bound the peak rather than the floor:
|
|
|
113
127
|
|
|
114
128
|
## Sizing a small sandbox
|
|
115
129
|
|
|
116
|
-
A worked example, for a 1.2GB container
|
|
130
|
+
A worked example, for a 1.2GB container running an app the size of `apps/minimal` (apps/akan's boot alone
|
|
131
|
+
peaks near 1.8GB):
|
|
117
132
|
|
|
118
133
|
```bash
|
|
119
134
|
AKAN_MEMORY_LIMIT=1200mb # builder gets ~420MB, rsc worker ~660MB
|
|
120
135
|
AKAN_DEV_IDLE_SUSPEND_MS=300000 # release the builder after 5 idle minutes
|
|
121
136
|
```
|
|
122
137
|
|
|
123
|
-
Two things to expect at that size. The builder crosses 420MB during ordinary work —
|
|
124
|
-
|
|
138
|
+
Two things to expect at that size. The builder crosses 420MB during ordinary work — its first route
|
|
139
|
+
build alone takes it to ~540MB — so it is replaced roughly once per 30s while you keep building, each
|
|
125
140
|
replacement costing a boot build that requests wait through rather than fail. And the build worker's
|
|
126
141
|
peak is not covered by any of these ceilings; if the kernel OOM-kills it, the dev server survives with a
|
|
127
|
-
red build for that generation and the log names the signal.
|
|
142
|
+
red build for that generation and the log names the signal. On a boot whose `.akan` was cleared, the SSR
|
|
143
|
+
registry's first build is such a worker, peaking near 900MB on `apps/minimal`: a killed one leaves no
|
|
144
|
+
registry, and every later save retries that same whole build, so a container that cannot fit it once
|
|
145
|
+
cannot fit it at all.
|
|
128
146
|
|
|
129
147
|
Raising `AKAN_BUILDER_MAX_RSS_MB` above the derived share trades memory for fewer boot builds. Setting
|
|
130
148
|
it to `0` trades the bound away entirely, which on a container this size means the kernel decides
|
package/README.ko.md
CHANGED
|
@@ -44,7 +44,7 @@ await runner.build();
|
|
|
44
44
|
- Frontend build transform과 RSC/SSR artifact builder.
|
|
45
45
|
- `@akanjs/cli`가 사용하는 command/script decorator.
|
|
46
46
|
- AI prompt, guideline, code-generation 지원 utility.
|
|
47
|
-
-
|
|
47
|
+
- `@akanjs/native` 기반 네이티브 모바일 빌드(iOS, Android)와 스토어 release helper.
|
|
48
48
|
|
|
49
49
|
## 패키지 경계
|
|
50
50
|
|
|
@@ -58,7 +58,7 @@ await runner.build();
|
|
|
58
58
|
|
|
59
59
|
- [Bun](https://bun.sh) `>=1.4.0`
|
|
60
60
|
- TypeScript
|
|
61
|
-
-
|
|
61
|
+
- `react`는 optional peer이며, 렌더링하는 기능에서만 필요합니다.
|
|
62
62
|
|
|
63
63
|
## 라이선스
|
|
64
64
|
|
package/README.md
CHANGED
|
@@ -43,7 +43,8 @@ await runner.build();
|
|
|
43
43
|
- Frontend build transforms and RSC/SSR artifact builders.
|
|
44
44
|
- Command/script decorators used by `@akanjs/cli`.
|
|
45
45
|
- AI prompt, guideline, and code-generation support utilities.
|
|
46
|
-
-
|
|
46
|
+
- Native mobile builds on `@akanjs/native` (iOS, Android) and store release helpers.
|
|
47
|
+
- The `akan code` agent engine, the tools it is given, and the shipped akan skills (`@akanjs/devkit/codeAgent`).
|
|
47
48
|
|
|
48
49
|
## Dev Server Sizing
|
|
49
50
|
|
|
@@ -62,7 +63,7 @@ defaults, the shares they derive from `AKAN_MEMORY_LIMIT`, and what a small cont
|
|
|
62
63
|
|
|
63
64
|
- [Bun](https://bun.sh) `>=1.4.0`
|
|
64
65
|
- TypeScript
|
|
65
|
-
-
|
|
66
|
+
- `react` is an optional peer, needed only by the features that render.
|
|
66
67
|
|
|
67
68
|
## License
|
|
68
69
|
|
package/abstractDoc.test.ts
CHANGED
|
@@ -1,29 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import os from "node:os";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
4
2
|
import path from "node:path";
|
|
5
3
|
import { AbstractDoc } from "./abstractDoc";
|
|
6
4
|
import { AppExecutor, WorkspaceExecutor } from "./executors";
|
|
5
|
+
import { tempDirs, writeText } from "./testHelpers";
|
|
7
6
|
|
|
8
|
-
const
|
|
7
|
+
const makeTempRoot = tempDirs("akan-abstract-doc-");
|
|
9
8
|
|
|
10
9
|
const makeApp = async (files: Record<string, string>) => {
|
|
11
|
-
const root = await
|
|
12
|
-
tempRoots.push(root);
|
|
10
|
+
const root = await makeTempRoot();
|
|
13
11
|
const appName = "abstractDemo";
|
|
14
|
-
for (const [filePath, content] of Object.entries(files))
|
|
15
|
-
|
|
16
|
-
await mkdir(path.dirname(absolutePath), { recursive: true });
|
|
17
|
-
await writeFile(absolutePath, content);
|
|
18
|
-
}
|
|
12
|
+
for (const [filePath, content] of Object.entries(files))
|
|
13
|
+
await writeText(path.join(root, "apps", appName, filePath), content);
|
|
19
14
|
const workspace = WorkspaceExecutor.fromRoot({ workspaceRoot: root, repoName: "repo" });
|
|
20
15
|
return AppExecutor.from(workspace, appName);
|
|
21
16
|
};
|
|
22
17
|
|
|
23
|
-
afterEach(async () => {
|
|
24
|
-
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
25
|
-
});
|
|
26
|
-
|
|
27
18
|
describe("AbstractDoc.kindOf", () => {
|
|
28
19
|
test("reads the module kind off the sys-relative path", () => {
|
|
29
20
|
expect(AbstractDoc.kindOf("lib/user/user.abstract.md")).toBe("domain");
|
package/abstractDoc.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
+
import path from "node:path";
|
|
1
2
|
import type { SysExecutor } from "./executors";
|
|
2
3
|
|
|
3
4
|
export type AbstractKind = "domain" | "service" | "scalar" | "other";
|
|
4
5
|
|
|
5
|
-
/** One `*.abstract.md` file: the agent-facing summary a module carries next to its source. */
|
|
6
6
|
export class AbstractDoc {
|
|
7
7
|
static readonly suffix = ".abstract.md";
|
|
8
|
-
/** `akan quality` warns above this
|
|
8
|
+
/** `akan quality` warns above this. */
|
|
9
9
|
static readonly maxLines = 300;
|
|
10
|
-
/** `akan compact` floor, and the line budget it compacts down to, so a compacted file is never a candidate again. */
|
|
11
|
-
static readonly compactMinLines = 40;
|
|
12
10
|
// Abstracts only live in facet folders; globbing the sys root instead would walk node_modules.
|
|
13
11
|
static readonly #facetRoots = "{lib,ui,webkit,srvkit,common,plugin}";
|
|
14
12
|
|
|
@@ -18,7 +16,6 @@ export class AbstractDoc {
|
|
|
18
16
|
static lineCountOf(content: string) {
|
|
19
17
|
return content.split(/\r?\n/).length;
|
|
20
18
|
}
|
|
21
|
-
/** Module kind read off the sys-relative path; anything outside a `lib/` module folder is "other". */
|
|
22
19
|
static kindOf(filePath: string): AbstractKind {
|
|
23
20
|
const [root, folder, ...rest] = filePath.split("/");
|
|
24
21
|
if (root !== "lib" || !folder) return "other";
|
|
@@ -27,7 +24,9 @@ export class AbstractDoc {
|
|
|
27
24
|
return folder.startsWith("_") ? "service" : "domain";
|
|
28
25
|
}
|
|
29
26
|
static async findAll(sys: SysExecutor, { module }: { module?: string | null } = {}) {
|
|
30
|
-
const filePaths = (await sys.getAllFiles(`${AbstractDoc.#facetRoots}/**/*${AbstractDoc.suffix}`))
|
|
27
|
+
const filePaths = (await sys.getAllFiles(`${AbstractDoc.#facetRoots}/**/*${AbstractDoc.suffix}`))
|
|
28
|
+
.map((filePath) => filePath.split(path.sep).join("/"))
|
|
29
|
+
.sort();
|
|
31
30
|
const docs = await Promise.all(filePaths.map((filePath) => AbstractDoc.read(sys, filePath)));
|
|
32
31
|
if (!module) return docs;
|
|
33
32
|
return docs.filter((doc) => doc.moduleName === module || doc.folderName === module);
|
|
@@ -59,8 +58,7 @@ export class AbstractDoc {
|
|
|
59
58
|
get lineCount() {
|
|
60
59
|
return AbstractDoc.lineCountOf(this.content);
|
|
61
60
|
}
|
|
62
|
-
// An AI editor
|
|
63
|
-
// replace the abstract — so a candidate must look like a markdown file and be shorter than what it replaces.
|
|
61
|
+
// An AI editor that decides nothing needs changing answers in prose, which must not replace the abstract.
|
|
64
62
|
canReplaceWith(candidate: string) {
|
|
65
63
|
const next = candidate.trim();
|
|
66
64
|
if (!next.startsWith("#")) return false;
|
package/agentsIndex.test.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
2
|
import path from "node:path";
|
|
5
3
|
import {
|
|
6
4
|
AGENT_BLOCK_END,
|
|
@@ -14,6 +12,7 @@ import {
|
|
|
14
12
|
upsertAgentBlock,
|
|
15
13
|
} from "./agentsIndex";
|
|
16
14
|
import type { RecipeInfo } from "./recipeScanner";
|
|
15
|
+
import { tempDirs, writeText } from "./testHelpers";
|
|
17
16
|
|
|
18
17
|
const button: RecipeInfo = {
|
|
19
18
|
name: "buttonRecipe",
|
|
@@ -79,12 +78,10 @@ describe("upsertAgentBlock / extractAgentBlock", () => {
|
|
|
79
78
|
});
|
|
80
79
|
|
|
81
80
|
describe("collectScopeRecipeSources", () => {
|
|
81
|
+
const makeTempRoot = tempDirs("agents-index-");
|
|
82
82
|
test("collects own + dependency lib recipes, never the framework's", async () => {
|
|
83
|
-
const root = await
|
|
84
|
-
const write = async (rel: string, content: string) =>
|
|
85
|
-
await mkdir(path.dirname(path.join(root, rel)), { recursive: true });
|
|
86
|
-
await writeFile(path.join(root, rel), content);
|
|
87
|
-
};
|
|
83
|
+
const root = await makeTempRoot();
|
|
84
|
+
const write = async (rel: string, content: string) => await writeText(path.join(root, rel), content);
|
|
88
85
|
await write("apps/minimal/ui/Recipe/appCard.ts", `export const appCard = recipe(tv({ base: "x" }));`);
|
|
89
86
|
await write("libs/shared/ui/Recipe/panel.ts", `export const panelRecipe = recipe(tv({ base: "y" }));`);
|
|
90
87
|
await write("pkgs/akanjs/ui/recipe/buttonRecipe.ts", `export const buttonRecipe = recipe(tv({ base: "z" }));`);
|
package/agentsIndex.ts
CHANGED
|
@@ -1,31 +1,18 @@
|
|
|
1
1
|
import type { RecipeInfo, RecipeSource } from "./recipeScanner";
|
|
2
2
|
|
|
3
|
-
// recipeScanner
|
|
4
|
-
// 상주하므로 스캔 스택은 첫 사용 시점에 지연 로드한다 — 정적 import 로 되돌리면 entryModuleGraph 테스트가 깨진다.
|
|
3
|
+
// Lazy: recipeScanner loads `typescript` (~65MB) and this module sits in the CLI entry graph (entryModuleGraph test).
|
|
5
4
|
let recipeScannerLoad: Promise<typeof import("./recipeScanner")> | null = null;
|
|
6
5
|
const loadRecipeScanner = () => (recipeScannerLoad ??= import("./recipeScanner"));
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
*
|
|
11
|
-
* 색인은 소유 경계로 쪼갠다: 루트 AGENTS.md 는 프레임워크(akanjs/ui) 레시피만 싣고, 각 앱/lib 은
|
|
12
|
-
* 자기 스코프에서 import 가능한 레시피(own + 의존 lib)를 자기 AGENTS.md 에 싣는다. 항상 로드되는
|
|
13
|
-
* 컨텍스트가 앱 수에 비례해 커지는 것과, import 불가능한 이웃 앱 레시피가 환상을 유발하는 것을 막는다.
|
|
14
|
-
*
|
|
15
|
-
* 신선도는 두 지점이 보장한다: `SysExecutor.scan(write)` 가 재생성하고(akan sync/build/start 가 전부
|
|
16
|
-
* 지나가는 길목), `akan lint` 가 스캔 결과와 커밋된 블록을 비교해 stale 이면 실패시킨다.
|
|
17
|
-
*/
|
|
7
|
+
//* Split by ownership (root: framework recipes; each app/lib: what it may import), so always-loaded context does not
|
|
8
|
+
//* grow with the app count and no unimportable neighbour recipe is listed.
|
|
18
9
|
|
|
19
10
|
export const AGENT_BLOCK_START = "<!-- akan:agent:start -->";
|
|
20
11
|
export const AGENT_BLOCK_END = "<!-- akan:agent:end -->";
|
|
21
12
|
|
|
22
13
|
const AGENT_VERSION_PREFIX = "<!-- akan:agent:version ";
|
|
23
14
|
|
|
24
|
-
/**
|
|
25
|
-
* The generating release, stamped into the workspace block. Nothing re-runs `akan agent install` on its own, so
|
|
26
|
-
* without a stamp a workspace carrying conventions from four releases ago is indistinguishable from a current one —
|
|
27
|
-
* `akan doctor` compares this against the installed devkit and says so.
|
|
28
|
-
*/
|
|
15
|
+
/** Nothing re-runs `akan agent install` on its own, so `akan doctor` compares this stamp against the installed devkit. */
|
|
29
16
|
export const stampBlockVersion = (block: string, version: string): string =>
|
|
30
17
|
`${AGENT_VERSION_PREFIX}${version} -->\n\n${block}`;
|
|
31
18
|
|
|
@@ -36,7 +23,7 @@ export const extractBlockVersion = (content: string): string | null =>
|
|
|
36
23
|
/** The running `@akanjs/devkit` version; null when its package.json is unreadable, which must not fail a doctor run. */
|
|
37
24
|
export const readDevkitVersion = async (): Promise<string | null> => {
|
|
38
25
|
const { readFile } = await import("node:fs/promises");
|
|
39
|
-
const { getDirname } = await import("./
|
|
26
|
+
const { getDirname } = await import("./fileSys");
|
|
40
27
|
try {
|
|
41
28
|
const raw = await readFile(`${getDirname(import.meta.url)}/package.json`, "utf-8");
|
|
42
29
|
return (JSON.parse(raw) as { version?: string }).version ?? null;
|
|
@@ -97,10 +84,7 @@ export const renderRecipeEntries = (recipes: RecipeInfo[]): string => {
|
|
|
97
84
|
.join("\n\n");
|
|
98
85
|
};
|
|
99
86
|
|
|
100
|
-
/**
|
|
101
|
-
* Every recipe source importable from the scope: its own `ui/Recipe/` plus each dependency lib's.
|
|
102
|
-
* Framework recipes are excluded on purpose — they live in the root AGENTS.md, valid for every scope.
|
|
103
|
-
*/
|
|
87
|
+
/** Own `ui/Recipe/` plus each dependency lib's; framework recipes live in the root AGENTS.md instead. */
|
|
104
88
|
export const collectScopeRecipeSources = async (
|
|
105
89
|
workspaceRoot: string,
|
|
106
90
|
scope: AgentsIndexScope,
|
|
@@ -1,34 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import os from "node:os";
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { realpath, rm, writeFile } from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import { Logger } from "akanjs/common";
|
|
6
5
|
import type { App } from "../commandDecorators";
|
|
6
|
+
import { tempDirs, writeText } from "../testHelpers";
|
|
7
7
|
import { BackendImportGraph } from "./BackendImportGraph";
|
|
8
8
|
import { filesChangedSince } from "./devHostPolicy";
|
|
9
9
|
|
|
10
10
|
describe("BackendImportGraph", () => {
|
|
11
|
-
const
|
|
11
|
+
const makeTempRoot = tempDirs("akan-devkit-graph-");
|
|
12
12
|
|
|
13
13
|
const makeGraph = async (files: Record<string, string>) => {
|
|
14
|
-
// Realpath
|
|
15
|
-
|
|
16
|
-
const workspaceRoot = await realpath(await mkdtemp(path.join(os.tmpdir(), "akan-devkit-graph-")));
|
|
17
|
-
tempRoots.push(workspaceRoot);
|
|
14
|
+
// Realpath: `Bun.resolveSync` returns real paths, and macOS `/var/folders` is a symlink.
|
|
15
|
+
const workspaceRoot = await realpath(await makeTempRoot());
|
|
18
16
|
const cwdPath = path.join(workspaceRoot, "apps/demo");
|
|
19
|
-
for (const [rel, source] of Object.entries(files))
|
|
20
|
-
const filePath = path.join(cwdPath, rel);
|
|
21
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
22
|
-
await writeFile(filePath, source);
|
|
23
|
-
}
|
|
17
|
+
for (const [rel, source] of Object.entries(files)) await writeText(path.join(cwdPath, rel), source);
|
|
24
18
|
const app = { cwdPath, workspace: { workspaceRoot } } as unknown as App;
|
|
25
19
|
return { graph: new BackendImportGraph(app, new Logger("test")), cwdPath };
|
|
26
20
|
};
|
|
27
21
|
|
|
28
|
-
afterEach(async () => {
|
|
29
|
-
await Promise.all(tempRoots.splice(0).map((root) => rm(root, { recursive: true, force: true })));
|
|
30
|
-
});
|
|
31
|
-
|
|
32
22
|
test("walks the backend entrypoints' import graph", async () => {
|
|
33
23
|
const { graph, cwdPath } = await makeGraph({
|
|
34
24
|
"main.ts": 'import "./server";\n',
|
|
@@ -51,7 +41,6 @@ describe("BackendImportGraph", () => {
|
|
|
51
41
|
await graph.refresh();
|
|
52
42
|
expect(graph.has(path.join(cwdPath, "lib/added.ts"))).toBe(false);
|
|
53
43
|
|
|
54
|
-
// The scan cache is keyed on (mtimeMs, size), so the rewrite must invalidate it.
|
|
55
44
|
await writeFile(path.join(cwdPath, "server.ts"), 'import "./lib/added";\nexport default 1;\n');
|
|
56
45
|
|
|
57
46
|
await graph.refresh();
|
|
@@ -81,8 +70,7 @@ describe("BackendImportGraph", () => {
|
|
|
81
70
|
await graph.refresh();
|
|
82
71
|
const before = await graph.fingerprint();
|
|
83
72
|
|
|
84
|
-
//
|
|
85
|
-
// the stamps are taken. `mtimeMs` has a coarse clock on Linux, so the size has to move too.
|
|
73
|
+
// `mtimeMs` has a coarse clock on Linux, so the size has to move too.
|
|
86
74
|
await writeFile(path.join(cwdPath, "lib/handler.ts"), "export const handler = () => 'changed';\n");
|
|
87
75
|
|
|
88
76
|
expect(filesChangedSince(before, await graph.fingerprint())).toEqual([path.join(cwdPath, "lib/handler.ts")]);
|
|
@@ -96,11 +84,9 @@ describe("BackendImportGraph", () => {
|
|
|
96
84
|
});
|
|
97
85
|
await graph.refresh();
|
|
98
86
|
const before = await graph.fingerprint();
|
|
99
|
-
// A recycle with no edit in it is the common case, and it must not cost a backend restart.
|
|
100
87
|
expect(filesChangedSince(before, await graph.fingerprint())).toEqual([]);
|
|
101
88
|
|
|
102
89
|
await rm(path.join(cwdPath, "lib/handler.ts"));
|
|
103
|
-
// Deleted counts as changed: the backend is still running what used to be there.
|
|
104
90
|
expect(filesChangedSince(before, await graph.fingerprint())).toEqual([path.join(cwdPath, "lib/handler.ts")]);
|
|
105
91
|
});
|
|
106
92
|
|
|
@@ -22,14 +22,7 @@ export class BackendImportGraph {
|
|
|
22
22
|
readonly #jsTranspiler = new Bun.Transpiler({ loader: "js" });
|
|
23
23
|
readonly #jsxTranspiler = new Bun.Transpiler({ loader: "jsx" });
|
|
24
24
|
#files = new Set<string>();
|
|
25
|
-
|
|
26
|
-
* `refresh()` runs on every server-side save and on every dev-host recycle, and re-reading plus
|
|
27
|
-
* re-parsing files that did not change is the whole cost of it. Keyed on (mtimeMs, size).
|
|
28
|
-
*
|
|
29
|
-
* Specifiers are cached, not resolved paths: creating a file can change what an *unchanged* importer's
|
|
30
|
-
* specifier resolves to, and `Bun.resolveSync` is cheap next to a read plus a transpiler scan. Only
|
|
31
|
-
* the scan result is retained — never the source text.
|
|
32
|
-
*/
|
|
25
|
+
// Specifiers, not resolved paths: a new file can change what an unchanged importer's specifier resolves to.
|
|
33
26
|
#scanCache = new Map<string, { mtimeMs: number; size: number; specifiers: Bun.Import[] }>();
|
|
34
27
|
#ready = false;
|
|
35
28
|
#lastRefreshSucceeded = false;
|
|
@@ -51,17 +44,6 @@ export class BackendImportGraph {
|
|
|
51
44
|
return this.#files.has(path.resolve(file));
|
|
52
45
|
}
|
|
53
46
|
|
|
54
|
-
/**
|
|
55
|
-
* Stamp every file the backend runs, so a caller can ask later what moved.
|
|
56
|
-
*
|
|
57
|
-
* Taken when the builder goes away and compared when its replacement is up, because nothing watches
|
|
58
|
-
* the tree in between: the departing builder's watcher left with it, and the replacement's index
|
|
59
|
-
* primes from the disk it finds, so an edit that lands in the gap is *baseline* to it and is never
|
|
60
|
-
* reported at all. The client half of such an edit is rescued by the replacement's boot build; the
|
|
61
|
-
* backend half is a server left running code that no longer exists, with nothing on screen to say so.
|
|
62
|
-
*
|
|
63
|
-
* One `stat` per graph file, against a gap that costs a whole boot build anyway.
|
|
64
|
-
*/
|
|
65
47
|
async fingerprint(): Promise<SourceFingerprints> {
|
|
66
48
|
const stamps = await Promise.all(
|
|
67
49
|
[...this.#files].map(async (file) => {
|
|
@@ -117,7 +99,6 @@ export class BackendImportGraph {
|
|
|
117
99
|
return files;
|
|
118
100
|
}
|
|
119
101
|
|
|
120
|
-
/** Null when the file is gone, which is the existence check the walk used to make separately. */
|
|
121
102
|
async #importsOf(file: string): Promise<Bun.Import[] | null> {
|
|
122
103
|
const stats = await stat(file).catch(() => null);
|
|
123
104
|
if (!stats?.isFile()) return null;
|
|
@@ -1,18 +1,71 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { IncrementalBuilderHost } from "../incrementalBuilder";
|
|
4
|
+
import { createTempApp, isolateEnv, tempRoots, writeText } from "../testHelpers";
|
|
2
5
|
import { AkanAppHost } from "./akanApp.host";
|
|
6
|
+
import type { DevHostEvent } from "./devHostPolicy";
|
|
3
7
|
|
|
4
8
|
describe("readProcessRssBytes", () => {
|
|
5
9
|
test("reads this process's own rss", async () => {
|
|
6
10
|
const rssBytes = await AkanAppHost.readProcessRssBytes(process.pid);
|
|
7
11
|
if (rssBytes === null) throw new Error("expected to read this process's own rss");
|
|
8
|
-
// Loose bounds on purpose: the point is that it read a real number from the OS, not which number.
|
|
9
12
|
expect(rssBytes).toBeGreaterThan(1024 * 1024);
|
|
10
13
|
expect(rssBytes).toBeLessThan(64 * 1024 * 1024 * 1024);
|
|
11
14
|
});
|
|
12
15
|
|
|
13
|
-
//
|
|
14
|
-
// would read as "settled below the ceiling" and cancel a recycle that should happen.
|
|
16
|
+
// A 0 would read as "settled below the ceiling" and cancel a recycle that should happen.
|
|
15
17
|
test("returns null for a pid that does not exist", async () => {
|
|
16
18
|
expect(await AkanAppHost.readProcessRssBytes(2_147_483_646)).toBeNull();
|
|
17
19
|
});
|
|
18
20
|
});
|
|
21
|
+
|
|
22
|
+
describe("a gateway whose replica crash-loops", () => {
|
|
23
|
+
isolateEnv({ AKAN_PUBLIC_REPO_NAME: "repo", AKAN_PUBLIC_SERVE_DOMAIN: "localhost", AKAN_PUBLIC_ENV: "local" });
|
|
24
|
+
const track = tempRoots();
|
|
25
|
+
const crashLoop = "Backend replica 0/federation failed 3 consecutive boots; waiting for a code change to retry: boom";
|
|
26
|
+
const readyBuilder = {
|
|
27
|
+
status: "ready",
|
|
28
|
+
patcherOff: false,
|
|
29
|
+
start: ({ onReady }: { onReady?: () => void }) => onReady?.(),
|
|
30
|
+
send: () => true,
|
|
31
|
+
stop: () => undefined,
|
|
32
|
+
} as unknown as IncrementalBuilderHost;
|
|
33
|
+
|
|
34
|
+
test("fails the dev server at once, and leaves it running for the fix", async () => {
|
|
35
|
+
const { root, app } = track(await createTempApp("demo"));
|
|
36
|
+
await writeText(
|
|
37
|
+
path.join(root, "apps/demo/main.ts"),
|
|
38
|
+
[
|
|
39
|
+
`process.send?.({ type: "build-status", data: { generation: -1, phase: "backend", ok: false, files: [], message: ${JSON.stringify(crashLoop)} } });`,
|
|
40
|
+
"setInterval(() => undefined, 1_000);",
|
|
41
|
+
"",
|
|
42
|
+
].join("\n"),
|
|
43
|
+
);
|
|
44
|
+
const create = IncrementalBuilderHost.create;
|
|
45
|
+
IncrementalBuilderHost.create = async () => readyBuilder;
|
|
46
|
+
const events: DevHostEvent[] = [];
|
|
47
|
+
const failed = Promise.withResolvers<void>();
|
|
48
|
+
const host = new AkanAppHost(app, {
|
|
49
|
+
env: { ...process.env } as Record<string, string>,
|
|
50
|
+
onDevEvent: (event) => {
|
|
51
|
+
events.push(event);
|
|
52
|
+
if ("state" in event && event.state === "failed") failed.resolve();
|
|
53
|
+
},
|
|
54
|
+
});
|
|
55
|
+
let beforeStop: DevHostEvent[] = [];
|
|
56
|
+
try {
|
|
57
|
+
await host.start();
|
|
58
|
+
await failed.promise;
|
|
59
|
+
await Bun.sleep(300);
|
|
60
|
+
beforeStop = [...events];
|
|
61
|
+
} finally {
|
|
62
|
+
await host.stop();
|
|
63
|
+
IncrementalBuilderHost.create = create;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
expect(beforeStop).toEqual([
|
|
67
|
+
{ app: "demo", state: "starting" },
|
|
68
|
+
{ app: "demo", state: "failed", detail: crashLoop },
|
|
69
|
+
]);
|
|
70
|
+
});
|
|
71
|
+
});
|