@akanjs/devkit 3.0.0-beta.9 → 3.0.0
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 +21 -7
- package/README.md +17 -8
- 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 +439 -141
- package/akanConfig/akanConfig.ts +359 -275
- package/akanConfig/index.ts +1 -0
- package/akanConfig/types.ts +22 -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 +270 -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 +182 -0
- package/mobile/desktopBin.ts +144 -0
- package/mobile/desktopServerStage.test.ts +252 -0
- package/mobile/desktopServerStage.ts +178 -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 +184 -0
- package/mobile/nativeApp.ts +454 -0
- package/mobile/nativeAppLine.test.ts +69 -0
- package/mobile/nativeAppLine.ts +71 -0
- package/mobile/nativeConfig.test.ts +438 -0
- package/mobile/nativeConfig.ts +294 -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 +9 -25
- package/subspace.ts +48 -115
- package/subspaceConfig.ts +3 -11
- 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
|
@@ -1,33 +1,26 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import { ApplicationBuildReporter } from "@akanjs/devkit/applicationBuildReporter";
|
|
3
|
+
import { resolveSsrPageEntriesForApp } from "@akanjs/devkit/artifact/implicitRootLayout";
|
|
4
|
+
import { computeRouteSeedIndex, saveRouteSeedIndex } from "@akanjs/devkit/artifact/routeSeedIndex";
|
|
2
5
|
import type { App } from "@akanjs/devkit/commandDecorators";
|
|
3
|
-
// Subpath imports only,
|
|
4
|
-
// eager import is paid on every save. Measured on a 177-route app: `executors` 24ms, `frontendBuild`
|
|
5
|
-
// ~110ms, app config 5ms.
|
|
6
|
+
// Subpath imports only, as few as possible: spawned per generation, this process pays every import on every save.
|
|
6
7
|
import { AppExecutor, WorkspaceExecutor } from "@akanjs/devkit/executors";
|
|
7
8
|
import {
|
|
8
9
|
CsrArtifactBuilder,
|
|
10
|
+
CsrDevBundler,
|
|
9
11
|
CssCompiler,
|
|
10
12
|
FontOptimizer,
|
|
11
13
|
PagesBundleBuilder,
|
|
14
|
+
ServerGraphFile,
|
|
12
15
|
SsrBaseArtifactBuilder,
|
|
16
|
+
SsrDevBundler,
|
|
13
17
|
} from "@akanjs/devkit/frontendBuild";
|
|
14
18
|
import { Logger } from "akanjs/common";
|
|
15
|
-
import type { BuilderMessage, BuildPhase } from "akanjs/server";
|
|
19
|
+
import type { BuilderMessage, BuildPhase, HmrTrace } from "akanjs/server";
|
|
20
|
+
import { resolveDevCsrMode } from "akanjs/server/hmr/csrDevManifest";
|
|
16
21
|
import type { BuildBatchRequest, BuildBatchResult, OptimizedFonts, PagesBatchCssAssets } from "./buildBatchProtocol";
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
* One generation of frontend build work, in a process that exits when it is done.
|
|
20
|
-
*
|
|
21
|
-
* This exists for one reason: `Bun.build` retains native bundler arenas that the process never returns
|
|
22
|
-
* to the OS — `Bun.gc(true)` reclaims nothing and the JS heap stays flat while RSS climbs ~250MB per
|
|
23
|
-
* save. Exit is the only mechanism that gives that memory back, so the work that scales per save lives
|
|
24
|
-
* here rather than in the long-lived watcher.
|
|
25
|
-
*
|
|
26
|
-
* Nothing is cached here, by design. That costs less than it appears to: `CssCompiler` rebuilds its
|
|
27
|
-
* tailwind compilers on every `compileCss` call, and the watcher always asked for `refresh: true`, so
|
|
28
|
-
* there was no warm state to lose. What genuinely had to be preserved travels in the request — the
|
|
29
|
-
* validated page keys and the previous font optimization.
|
|
30
|
-
*/
|
|
23
|
+
// `Bun.build` keeps native bundler arenas that `Bun.gc(true)` cannot reclaim; only exiting returns them.
|
|
31
24
|
class BuildBatch {
|
|
32
25
|
#logger = new Logger("BuildBatch");
|
|
33
26
|
#request: BuildBatchRequest;
|
|
@@ -40,33 +33,27 @@ class BuildBatch {
|
|
|
40
33
|
}
|
|
41
34
|
|
|
42
35
|
async run(): Promise<BuildBatchResult> {
|
|
43
|
-
// `base` arrives alone, from a builder that cannot serve anything until it finishes.
|
|
44
36
|
if (this.#request.needs.includes("base")) await this.#buildBase();
|
|
45
|
-
// Ordered the way the watcher used to run them: csr before pages so a csr failure cannot delay the
|
|
46
|
-
// pages bundle the browser is waiting on, and css last because it depends on the rebuilt client.
|
|
47
37
|
if (this.#request.needs.includes("csr")) await this.#buildCsr();
|
|
48
|
-
if (this.#request.needs.includes("
|
|
38
|
+
if (this.#request.needs.includes("ssr")) await this.#buildSsrRegistry();
|
|
39
|
+
// Css before pages: it scans the sources and reads nothing pages produces, so a class edit need not wait for pages.
|
|
49
40
|
if (this.#request.needs.includes("css")) await this.#buildCss();
|
|
41
|
+
if (this.#request.needs.includes("pages")) await this.#buildPages();
|
|
50
42
|
return this.#result;
|
|
51
43
|
}
|
|
52
44
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
* pages bundle; making it wait for the css compile behind it would add latency the in-process version
|
|
56
|
-
* never had, and it would move every artifact write into the window right before the watcher reports
|
|
57
|
-
* the generation complete — which is where a save issued immediately afterwards gets dropped by Bun's
|
|
58
|
-
* recursive `fs.watch` (`local/optimize-resource/06-watcher-dropped-event.md`).
|
|
59
|
-
*
|
|
60
|
-
* A bare `process.send` is safe here, unlike in the watcher, for one reason: this process ends by
|
|
61
|
-
* returning from `main`, and a natural exit flushes a pending ipc write (measured: 1MB delivered
|
|
62
|
-
* 20/20). It is `process.exit` that discards one — so adding an explicit exit to this file, at the end
|
|
63
|
-
* of `main` or anywhere after an emit, would silently start dropping `css-updated` payloads. Route
|
|
64
|
-
* sends through `BuilderChannel` if that ever becomes necessary.
|
|
65
|
-
*/
|
|
45
|
+
// A bare `process.send` is safe only because this process exits by returning from `main`, which flushes ipc;
|
|
46
|
+
// an explicit `process.exit` after an emit would silently drop payloads (route through BuilderChannel then).
|
|
66
47
|
#emit(message: BuilderMessage): void {
|
|
67
48
|
process.send?.(message);
|
|
68
49
|
}
|
|
69
50
|
|
|
51
|
+
#sentTrace(): HmrTrace | undefined {
|
|
52
|
+
if (!this.#request.trace) return undefined;
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
return { ...this.#request.trace, patchAt: now, sentAt: now };
|
|
55
|
+
}
|
|
56
|
+
|
|
70
57
|
#emitStatus(phase: BuildPhase, message?: string): void {
|
|
71
58
|
this.#emit({
|
|
72
59
|
type: "build-status",
|
|
@@ -80,11 +67,15 @@ class BuildBatch {
|
|
|
80
67
|
});
|
|
81
68
|
}
|
|
82
69
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
70
|
+
// `Bun.build` rejects with an AggregateError whose own message is only "Bundle failed"; the reasons are nested.
|
|
71
|
+
#fail(need: keyof BuildBatchResult["errors"], label: string, err: unknown): void {
|
|
72
|
+
const message = ApplicationBuildReporter.formatError(err, this.#request.workspaceRoot);
|
|
73
|
+
this.#logger.error(`${label} failed: ${message}`);
|
|
74
|
+
this.#result.errors[need] = message;
|
|
75
|
+
if (need !== "base") this.#emitStatus(need, message);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Streams nothing: the builder is not serving yet, so the watcher reads the outcome from the batch result.
|
|
88
79
|
async #buildBase(): Promise<void> {
|
|
89
80
|
const started = Date.now();
|
|
90
81
|
try {
|
|
@@ -93,30 +84,115 @@ class BuildBatch {
|
|
|
93
84
|
this.#result.optimizedFonts = optimizedFonts;
|
|
94
85
|
this.#logger.verbose(`base-artifact ok buildId=${artifact.pagesBundleBuildId} (${Date.now() - started}ms)`);
|
|
95
86
|
} catch (err) {
|
|
96
|
-
|
|
97
|
-
this.#logger.error(`base-artifact failed: ${message}`);
|
|
98
|
-
this.#result.errors.base = message;
|
|
87
|
+
this.#fail("base", "base-artifact", err);
|
|
99
88
|
}
|
|
100
89
|
}
|
|
101
90
|
|
|
102
91
|
async #buildCsr(): Promise<void> {
|
|
103
92
|
const started = Date.now();
|
|
104
93
|
try {
|
|
105
|
-
await
|
|
106
|
-
this.#
|
|
94
|
+
if (resolveDevCsrMode() === "registry") await this.#updateCsrRegistry(started);
|
|
95
|
+
else await this.#rebuildCsrArtifact(started);
|
|
107
96
|
this.#emitStatus("csr");
|
|
108
97
|
} catch (err) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
98
|
+
this.#fail("csr", "csr-rebundle", err);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async #rebuildCsrArtifact(started: number): Promise<void> {
|
|
103
|
+
await new CsrArtifactBuilder(this.#app).build();
|
|
104
|
+
this.#logger.verbose(`csr-rebundle ok (${Date.now() - started}ms)`);
|
|
105
|
+
// A CSR tab takes none of the SSR refresh messages, so a rebuilt artifact reaches it only as this reload.
|
|
106
|
+
if (this.#request.changedFiles.length === 0) return;
|
|
107
|
+
this.#emit({
|
|
108
|
+
type: "csr-updated",
|
|
109
|
+
data: { generation: Date.now(), mode: "artifact", reload: true, reason: "the CSR artifact was rebuilt" },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async #updateCsrRegistry(started: number): Promise<void> {
|
|
114
|
+
const update = await new CsrDevBundler(this.#app).update(this.#request.changedFiles, {
|
|
115
|
+
announce: (announced) =>
|
|
116
|
+
this.#emit({
|
|
117
|
+
type: "csr-updated",
|
|
118
|
+
data: {
|
|
119
|
+
generation: announced.generation,
|
|
120
|
+
mode: "registry",
|
|
121
|
+
reload: announced.reload,
|
|
122
|
+
reason: announced.reason,
|
|
123
|
+
patchUrl: announced.patchUrl,
|
|
124
|
+
changedIds: announced.changedIds,
|
|
125
|
+
trace: this.#sentTrace(),
|
|
126
|
+
},
|
|
127
|
+
}),
|
|
128
|
+
});
|
|
129
|
+
if (!update) {
|
|
130
|
+
this.#logger.verbose(`csr-dev unchanged (${Date.now() - started}ms)`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
this.#logger.verbose(
|
|
134
|
+
`csr-dev generation=${update.generation} ${update.reload ? `reload (${update.reason})` : `patch modules=${update.changedIds.length}`} graph=${update.moduleCount} (${Date.now() - started}ms)`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Takes every entry the routes reach as a root, so a route build naming any of them finds it in the registry.
|
|
139
|
+
async #buildSsrRegistry(): Promise<void> {
|
|
140
|
+
const started = Date.now();
|
|
141
|
+
try {
|
|
142
|
+
const bundler = new SsrDevBundler(this.#app);
|
|
143
|
+
//? Runs before pages, so the graph is the last build's, as in the resident builder: a save it holds still waits.
|
|
144
|
+
const hold = await ServerGraphFile.touches(
|
|
145
|
+
await ServerGraphFile.read(this.#request.artifactDir),
|
|
146
|
+
this.#request.changedFiles,
|
|
147
|
+
(file) => ServerGraphFile.clientExportsOf(file),
|
|
148
|
+
);
|
|
149
|
+
const update = await bundler.update(this.#request.changedFiles, {
|
|
150
|
+
roots: await bundler.clientEntries(),
|
|
151
|
+
announce: (announced) =>
|
|
152
|
+
this.#emit({
|
|
153
|
+
type: "ssr-updated",
|
|
154
|
+
data: {
|
|
155
|
+
generation: announced.generation,
|
|
156
|
+
reload: announced.reload,
|
|
157
|
+
reason: announced.reason,
|
|
158
|
+
patchUrl: announced.patchUrl,
|
|
159
|
+
changedIds: announced.changedIds,
|
|
160
|
+
trace: this.#sentTrace(),
|
|
161
|
+
...(announced.epoch !== undefined ? { epoch: announced.epoch } : {}),
|
|
162
|
+
...(announced.first ? { first: true } : {}),
|
|
163
|
+
...(hold && this.#request.changedFiles.length > 0
|
|
164
|
+
? { hold, batchGeneration: this.#request.generation }
|
|
165
|
+
: {}),
|
|
166
|
+
},
|
|
167
|
+
}),
|
|
168
|
+
});
|
|
169
|
+
this.#emitStatus("ssr");
|
|
170
|
+
this.#logger.verbose(
|
|
171
|
+
update
|
|
172
|
+
? `ssr-dev generation=${update.generation} ${update.reload ? `reload (${update.reason})` : `patch modules=${update.changedIds.length}`} graph=${update.moduleCount} (${Date.now() - started}ms)`
|
|
173
|
+
: `ssr-dev unchanged (${Date.now() - started}ms)`,
|
|
174
|
+
);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
this.#fail("ssr", "ssr-dev", err);
|
|
113
177
|
}
|
|
114
178
|
}
|
|
115
179
|
|
|
180
|
+
// Rewritten with the bundle: the backend rereads it on `pages-updated` to pick up added, moved or deleted routes.
|
|
116
181
|
async #buildPages(): Promise<void> {
|
|
117
182
|
const started = Date.now();
|
|
118
183
|
try {
|
|
119
|
-
const
|
|
184
|
+
const pageEntries = await resolveSsrPageEntriesForApp(this.#app, await this.#app.getPageKeys());
|
|
185
|
+
const seedIndex = computeRouteSeedIndex(pageEntries);
|
|
186
|
+
const previousGraph = await ServerGraphFile.read(this.#request.artifactDir);
|
|
187
|
+
const next = await new PagesBundleBuilder(this.#app, "start", pageEntries).build();
|
|
188
|
+
await saveRouteSeedIndex(this.#request.artifactDir, seedIndex);
|
|
189
|
+
const nextGraph = await ServerGraphFile.read(this.#request.artifactDir);
|
|
190
|
+
const serverTouched = await ServerGraphFile.touches(
|
|
191
|
+
previousGraph,
|
|
192
|
+
[...this.#request.changedFiles, ...(previousGraph?.carried ?? [])],
|
|
193
|
+
(file) => nextGraph?.clientExports[file] ?? null,
|
|
194
|
+
nextGraph,
|
|
195
|
+
);
|
|
120
196
|
this.#emit({
|
|
121
197
|
type: "pages-updated",
|
|
122
198
|
data: {
|
|
@@ -124,15 +200,17 @@ class BuildBatch {
|
|
|
124
200
|
buildId: next.buildId,
|
|
125
201
|
generation: this.#request.generation,
|
|
126
202
|
changedFiles: this.#request.changedFiles,
|
|
203
|
+
trace: this.#sentTrace(),
|
|
204
|
+
serverTouched,
|
|
127
205
|
},
|
|
128
206
|
});
|
|
129
207
|
this.#emitStatus("pages");
|
|
130
208
|
this.#logger.verbose(`pages-rebundle ok buildId=${next.buildId} (${Date.now() - started}ms)`);
|
|
131
209
|
} catch (err) {
|
|
132
|
-
|
|
133
|
-
this.#
|
|
134
|
-
|
|
135
|
-
|
|
210
|
+
this.#fail("pages", "pages-rebundle", err);
|
|
211
|
+
await ServerGraphFile.carry(this.#request.artifactDir, this.#request.changedFiles).catch((carryError: unknown) =>
|
|
212
|
+
this.#logger.warn(`server graph carry failed; the next save may skip its RSC refresh: ${String(carryError)}`),
|
|
213
|
+
);
|
|
136
214
|
}
|
|
137
215
|
}
|
|
138
216
|
|
|
@@ -174,14 +252,10 @@ class BuildBatch {
|
|
|
174
252
|
});
|
|
175
253
|
this.#logger.verbose(`css-compile ok assets=${Object.keys(cssAssets).length} (${Date.now() - started}ms)`);
|
|
176
254
|
} catch (err) {
|
|
177
|
-
|
|
178
|
-
this.#logger.error(`css-rebuild failed: ${message}`);
|
|
179
|
-
this.#result.errors.css = message;
|
|
180
|
-
this.#emitStatus("css", message);
|
|
255
|
+
this.#fail("css", "css-rebuild", err);
|
|
181
256
|
}
|
|
182
257
|
}
|
|
183
258
|
|
|
184
|
-
/** Fonts are expensive and rarely change, so the previous result is reused unless this batch touched it. */
|
|
185
259
|
async #optimizeFonts(): Promise<OptimizedFonts> {
|
|
186
260
|
const previous = this.#request.optimizedFonts;
|
|
187
261
|
if (previous && !BuildBatch.#shouldReoptimizeFonts(previous, this.#request.changedFiles)) {
|
|
@@ -196,7 +270,6 @@ class BuildBatch {
|
|
|
196
270
|
}
|
|
197
271
|
|
|
198
272
|
static #shouldReoptimizeFonts(previous: OptimizedFonts, changedFiles: string[]): boolean {
|
|
199
|
-
if (changedFiles.length === 0) return false;
|
|
200
273
|
return changedFiles.some((file) => {
|
|
201
274
|
const normalized = path.resolve(file);
|
|
202
275
|
if (/\.(woff2?|ttf|otf)$/i.test(normalized)) return true;
|
|
@@ -207,21 +280,32 @@ class BuildBatch {
|
|
|
207
280
|
static async main(): Promise<void> {
|
|
208
281
|
const raw = process.argv[2];
|
|
209
282
|
if (!raw) throw new Error("[build-batch] missing request argument");
|
|
210
|
-
const
|
|
283
|
+
const parsed = JSON.parse(raw) as BuildBatchRequest;
|
|
284
|
+
const request = parsed.trace
|
|
285
|
+
? {
|
|
286
|
+
...parsed,
|
|
287
|
+
trace: { ...parsed.trace, workerStartAt: Math.round(performance.timeOrigin), workerAt: Date.now() },
|
|
288
|
+
}
|
|
289
|
+
: parsed;
|
|
211
290
|
const workspace = WorkspaceExecutor.fromRoot({
|
|
212
291
|
workspaceRoot: request.workspaceRoot,
|
|
213
292
|
repoName: request.repoName,
|
|
214
293
|
});
|
|
215
294
|
const app = AppExecutor.from(workspace, request.appName);
|
|
216
|
-
// Seeded
|
|
217
|
-
// and repeating that here would be the single largest cost of spawning this process.
|
|
295
|
+
// Seeded, not rediscovered: route discovery would be the largest cost of spawning this process.
|
|
218
296
|
if (request.pageKeys) app.setPageKeys(request.pageKeys);
|
|
297
|
+
//? Exits with the builder that spawned it (a kill, a crash, a restart for a metadata save): left running, it would
|
|
298
|
+
//? write a registry its replacement is rebuilding. Removed before returning, since the listener keeps Bun's IPC open.
|
|
299
|
+
const orphaned = () => process.exit(1);
|
|
300
|
+
process.on("disconnect", orphaned);
|
|
219
301
|
const result = await new BuildBatch(request, app).run();
|
|
220
302
|
process.send?.({ type: "build-batch-result", data: result });
|
|
303
|
+
process.off("disconnect", orphaned);
|
|
221
304
|
}
|
|
222
305
|
}
|
|
223
306
|
|
|
224
|
-
|
|
307
|
+
// Awaited, not voided: on Windows Bun exits during a missing file's `Bun.file` read unless an entry await is pending.
|
|
308
|
+
await BuildBatch.main().catch((err) => {
|
|
225
309
|
console.error(err);
|
|
226
310
|
process.exit(1);
|
|
227
311
|
});
|
|
@@ -1,25 +1,12 @@
|
|
|
1
1
|
import type { FontOptimizer } from "@akanjs/devkit/frontendBuild";
|
|
2
|
-
import type { BaseBuildArtifact, CssPayload, PagesBundlePayload } from "akanjs/server";
|
|
2
|
+
import type { BaseBuildArtifact, CssPayload, HmrTrace, PagesBundlePayload } from "akanjs/server";
|
|
3
3
|
|
|
4
4
|
export type OptimizedFonts = Awaited<ReturnType<FontOptimizer["optimize"]>>;
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* builder runs before it can serve anything, where the rest are per-save work. It earns its place here
|
|
9
|
-
* for the same reason as the others — measured on `apps/akan`, it retains **+1143 MB** of bundler arena
|
|
10
|
-
* that only a process exit returns, which was 65 % of the builder's whole idle footprint.
|
|
11
|
-
*/
|
|
12
|
-
export type BuildBatchNeed = "base" | "pages" | "css" | "csr";
|
|
6
|
+
// `base` is the boot build: it always runs alone, before the builder can serve anything.
|
|
7
|
+
export type BuildBatchNeed = "base" | "pages" | "css" | "csr" | "ssr";
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
* One generation of build work, handed to a process that exits when it is done.
|
|
16
|
-
*
|
|
17
|
-
* Everything here is JSON: the worker is spawned per batch, so state that the long-lived watcher
|
|
18
|
-
* caches for the session has to travel by value. Two fields exist purely to keep behaviour identical
|
|
19
|
-
* to the in-process version — `pageKeys` because rediscovering them costs ~220ms on a 177-route app
|
|
20
|
-
* (route-export validation), and `optimizedFonts` because fonts are only re-optimized when a font file
|
|
21
|
-
* actually changed.
|
|
22
|
-
*/
|
|
9
|
+
// JSON only: the worker is spawned per batch and receives this in its argv.
|
|
23
10
|
export interface BuildBatchRequest {
|
|
24
11
|
appName: string;
|
|
25
12
|
workspaceRoot: string;
|
|
@@ -27,37 +14,25 @@ export interface BuildBatchRequest {
|
|
|
27
14
|
generation: number;
|
|
28
15
|
needs: BuildBatchNeed[];
|
|
29
16
|
changedFiles: string[];
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
optimizedFonts: OptimizedFonts | null;
|
|
34
|
-
/** Previous css assets, so an unchanged compile skips the broadcast instead of busting hashes. */
|
|
35
|
-
cssAssets: PagesBatchCssAssets | null;
|
|
36
|
-
/** Absolute artifact directory; the worker writes css assets under it. */
|
|
17
|
+
pageKeys: string[] | null; // null: the worker discovers them itself
|
|
18
|
+
optimizedFonts: OptimizedFonts | null; // reused unless this batch touched one of its files
|
|
19
|
+
cssAssets: PagesBatchCssAssets | null; // an unchanged compile skips the broadcast instead of busting hashes
|
|
37
20
|
artifactDir: string;
|
|
21
|
+
trace?: HmrTrace;
|
|
38
22
|
}
|
|
39
23
|
|
|
40
24
|
export type PagesBatchCssAssets = CssPayload["cssAssets"];
|
|
41
25
|
|
|
42
|
-
|
|
43
|
-
* What the watcher folds back into its own state once the worker is done.
|
|
44
|
-
*
|
|
45
|
-
* Deliberately small: the payloads browsers are waiting on are *streamed* as each need finishes
|
|
46
|
-
* (`pages-updated`, `css-updated`, `build-status`, relayed straight through), so a page reload is not
|
|
47
|
-
* held back by a css compile that has not started yet. Only state the next batch needs travels here.
|
|
48
|
-
*/
|
|
26
|
+
// Browser payloads stream as each need finishes; only state the next batch needs travels back here.
|
|
49
27
|
export interface BuildBatchResult {
|
|
50
28
|
generation: number;
|
|
51
29
|
cssAssets?: PagesBatchCssAssets;
|
|
52
30
|
optimizedFonts?: OptimizedFonts;
|
|
53
|
-
|
|
54
|
-
* Present only for a `base` batch. The same object the worker wrote to `base-artifact.json`, returned
|
|
55
|
-
* here so the watcher does not have to read its own boot build back off disk.
|
|
56
|
-
*/
|
|
57
|
-
artifact?: BaseBuildArtifact;
|
|
31
|
+
artifact?: BaseBuildArtifact; // `base` batches only
|
|
58
32
|
errors: Partial<Record<BuildBatchNeed, string>>;
|
|
59
|
-
|
|
60
|
-
crashed
|
|
33
|
+
crashed?: boolean; // died before reporting its result
|
|
34
|
+
/** Of a crashed batch, the needs it died before reporting: the others streamed their build-status first. */
|
|
35
|
+
crashedNeeds?: BuildBatchNeed[];
|
|
61
36
|
}
|
|
62
37
|
|
|
63
38
|
export type BuildBatchMessage = { type: "build-batch-result"; data: BuildBatchResult };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createTempApp, tempRoots, writeText } from "../testHelpers";
|
|
4
|
+
import { BuildBatchRunner } from "./buildBatchRunner";
|
|
5
|
+
|
|
6
|
+
const track = tempRoots();
|
|
7
|
+
|
|
8
|
+
describe("BuildBatchRunner", () => {
|
|
9
|
+
test("reports a boot build the bundler refuses with where each reason sits in the workspace", async () => {
|
|
10
|
+
// Inside the checkout so `akanjs/server` resolves to its source: from the OS temp dir Bun auto-installs npm's 2.x.
|
|
11
|
+
const { root } = track(await createTempApp("demo", path.join(import.meta.dir, "..", "local")));
|
|
12
|
+
const appDir = path.join(root, "apps/demo");
|
|
13
|
+
await writeText(path.join(appDir, "env/env.client.ts"), "export const env = {} as const;\n");
|
|
14
|
+
await writeText(
|
|
15
|
+
path.join(appDir, "page/_index.tsx"),
|
|
16
|
+
'import { gone } from "./not-there";\nexport default gone;\n',
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
const result = await new BuildBatchRunner({ workspaceRoot: root, cwd: appDir }).run({
|
|
20
|
+
appName: "demo",
|
|
21
|
+
workspaceRoot: root,
|
|
22
|
+
repoName: "repo",
|
|
23
|
+
generation: 0,
|
|
24
|
+
needs: ["base"],
|
|
25
|
+
changedFiles: [],
|
|
26
|
+
pageKeys: null,
|
|
27
|
+
optimizedFonts: null,
|
|
28
|
+
cssAssets: null,
|
|
29
|
+
artifactDir: path.join(appDir, ".akan/artifact"),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(result.errors.base).toContain('"./not-there" (apps/demo/page/_index.tsx:1:22)');
|
|
33
|
+
}, 30_000);
|
|
34
|
+
|
|
35
|
+
test("a worker that dies mid-batch keeps what each need reported, and fails only the needs it never reached", async () => {
|
|
36
|
+
const { root } = track(await createTempApp("demo"));
|
|
37
|
+
const appDir = path.join(root, "apps/demo");
|
|
38
|
+
await writeText(
|
|
39
|
+
path.join(root, "pkgs/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
|
|
40
|
+
`const { generation } = JSON.parse(process.argv[2]);
|
|
41
|
+
const status = (phase, message) =>
|
|
42
|
+
process.send({ type: "build-status", data: { generation, phase, ok: !message, files: [], message } });
|
|
43
|
+
status("ssr");
|
|
44
|
+
status("css", "css broke");
|
|
45
|
+
setTimeout(() => process.exit(1), 50);
|
|
46
|
+
`,
|
|
47
|
+
);
|
|
48
|
+
const result = await new BuildBatchRunner({ workspaceRoot: root, cwd: appDir }).run({
|
|
49
|
+
appName: "demo",
|
|
50
|
+
workspaceRoot: root,
|
|
51
|
+
repoName: "repo",
|
|
52
|
+
generation: 7,
|
|
53
|
+
needs: ["ssr", "css", "pages"],
|
|
54
|
+
changedFiles: [],
|
|
55
|
+
pageKeys: null,
|
|
56
|
+
optimizedFonts: null,
|
|
57
|
+
cssAssets: null,
|
|
58
|
+
artifactDir: path.join(appDir, ".akan/artifact"),
|
|
59
|
+
});
|
|
60
|
+
expect(result).toMatchObject({ crashed: true, crashedNeeds: ["pages"] });
|
|
61
|
+
expect(result.errors.ssr).toBeUndefined();
|
|
62
|
+
expect(result.errors.css).toBe("css broke");
|
|
63
|
+
expect(result.errors.pages).toContain("exited with code 1");
|
|
64
|
+
}, 30_000);
|
|
65
|
+
});
|
|
@@ -3,25 +3,10 @@ import { Logger } from "akanjs/common";
|
|
|
3
3
|
import type { BuilderMessage } from "akanjs/server";
|
|
4
4
|
import type { BuildBatchMessage, BuildBatchRequest, BuildBatchResult } from "./buildBatchProtocol";
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
* The watcher serializes every batch through its own work queue, so this deliberately has no pool: one
|
|
10
|
-
* worker exists at a time, and it exits before the next one starts. That is the whole point — the
|
|
11
|
-
* bundler arenas `Bun.build` never frees go back to the OS with the process.
|
|
12
|
-
*
|
|
13
|
-
* A worker that dies without reporting is not fatal. Every need it was given comes back as an error, the
|
|
14
|
-
* watcher reports a red build-status for that generation, and the last-good artifact keeps serving —
|
|
15
|
-
* the same contract a failed in-process build had. It must never take the watcher down with it: the
|
|
16
|
-
* watcher is the dev server's file watcher, so nothing would notice the fix.
|
|
17
|
-
*/
|
|
6
|
+
// No pool on purpose: a worker exiting after its batch returns the arenas `Bun.build` never frees. The callers order
|
|
7
|
+
// their own work (the slow lane, the css queue, the SSR registry's boot build beside them), so a few can overlap.
|
|
8
|
+
// A worker that dies without reporting must not take the watcher down; its needs come back as errors.
|
|
18
9
|
export class BuildBatchRunner {
|
|
19
|
-
static readonly #entryCandidates = (workspaceRoot: string) => [
|
|
20
|
-
path.join(workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
|
|
21
|
-
path.join(workspaceRoot, "node_modules/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
|
|
22
|
-
path.join(import.meta.dir, "buildBatch.proc.js"),
|
|
23
|
-
path.join(import.meta.dir, "buildBatch.proc.ts"),
|
|
24
|
-
];
|
|
25
10
|
#logger = new Logger("BuildBatchRunner");
|
|
26
11
|
#entry: string | null = null;
|
|
27
12
|
#workspaceRoot: string;
|
|
@@ -33,7 +18,12 @@ export class BuildBatchRunner {
|
|
|
33
18
|
|
|
34
19
|
async #resolveEntry(): Promise<string> {
|
|
35
20
|
if (this.#entry) return this.#entry;
|
|
36
|
-
const candidates =
|
|
21
|
+
const candidates = [
|
|
22
|
+
path.join(this.#workspaceRoot, "pkgs/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
|
|
23
|
+
path.join(this.#workspaceRoot, "node_modules/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
|
|
24
|
+
path.join(import.meta.dir, "buildBatch.proc.js"),
|
|
25
|
+
path.join(import.meta.dir, "buildBatch.proc.ts"),
|
|
26
|
+
];
|
|
37
27
|
for (const candidate of candidates) {
|
|
38
28
|
if (!(await Bun.file(candidate).exists())) continue;
|
|
39
29
|
this.#entry = candidate;
|
|
@@ -42,11 +32,6 @@ export class BuildBatchRunner {
|
|
|
42
32
|
throw new Error(`[build-batch] worker entry not found; looked in: ${candidates.join(", ")}`);
|
|
43
33
|
}
|
|
44
34
|
|
|
45
|
-
/**
|
|
46
|
-
* `onMessage` receives everything the worker streams as it goes — `pages-updated`, `css-updated`,
|
|
47
|
-
* `build-status` — so the watcher can relay each one the moment it is produced instead of holding a
|
|
48
|
-
* page reload until the whole batch is done.
|
|
49
|
-
*/
|
|
50
35
|
async run(
|
|
51
36
|
request: BuildBatchRequest,
|
|
52
37
|
onMessage: (message: BuilderMessage) => void = () => undefined,
|
|
@@ -54,9 +39,10 @@ export class BuildBatchRunner {
|
|
|
54
39
|
const started = Date.now();
|
|
55
40
|
const entry = await this.#resolveEntry();
|
|
56
41
|
let result: BuildBatchResult | null = null;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
42
|
+
const reported = new Map<string, string | undefined>();
|
|
43
|
+
const payload = request.trace ? { ...request, trace: { ...request.trace, spawnAt: Date.now() } } : request;
|
|
44
|
+
// argv rather than IPC, so the worker starts on its first tick instead of waiting for a handshake.
|
|
45
|
+
const proc = Bun.spawn(["bun", entry, JSON.stringify(payload)], {
|
|
60
46
|
cwd: this.#cwd,
|
|
61
47
|
env: process.env,
|
|
62
48
|
stdio: ["ignore", "inherit", "inherit"],
|
|
@@ -64,7 +50,10 @@ export class BuildBatchRunner {
|
|
|
64
50
|
ipc: (message: BuildBatchMessage | BuilderMessage) => {
|
|
65
51
|
if (!message || typeof message !== "object") return;
|
|
66
52
|
if (message.type === "build-batch-result") result = message.data;
|
|
67
|
-
else
|
|
53
|
+
else {
|
|
54
|
+
if (message.type === "build-status") reported.set(message.data.phase, message.data.message);
|
|
55
|
+
onMessage(message);
|
|
56
|
+
}
|
|
68
57
|
},
|
|
69
58
|
});
|
|
70
59
|
const exitCode = await proc.exited;
|
|
@@ -74,21 +63,20 @@ export class BuildBatchRunner {
|
|
|
74
63
|
);
|
|
75
64
|
return result;
|
|
76
65
|
}
|
|
77
|
-
//
|
|
78
|
-
// reads exactly like an ordinary crash — and the two have opposite fixes: one is a build error to
|
|
79
|
-
// find, the other is a memory limit to raise. The peak here is the largest transient in the tree
|
|
80
|
-
// (a boot build measured 548MB on a tenant app, 1.1GB on apps/akan), so on a small sandbox this is
|
|
81
|
-
// the process the kernel reaches for first.
|
|
66
|
+
// An OOM kill (code null, SIGKILL) reads like a crash without the signal, and needs the opposite fix.
|
|
82
67
|
const message = proc.signalCode
|
|
83
68
|
? `build worker was killed by ${proc.signalCode} before reporting a result${
|
|
84
69
|
proc.signalCode === "SIGKILL" ? " — most often the kernel OOM killer; check the sandbox's memory limit" : ""
|
|
85
70
|
}`
|
|
86
71
|
: `build worker exited with code ${exitCode} before reporting a result`;
|
|
87
72
|
this.#logger.error(`[build-batch] generation=${request.generation} ${message}`);
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
73
|
+
//? A need that reported before the crash keeps what it reported: a worker killed during pages built ssr and css.
|
|
74
|
+
const crashedNeeds = request.needs.filter((need) => !reported.has(need));
|
|
75
|
+
const failed = request.needs.flatMap((need) => {
|
|
76
|
+
if (!reported.has(need)) return [[need, message] as const];
|
|
77
|
+
const error = reported.get(need);
|
|
78
|
+
return error ? [[need, error] as const] : [];
|
|
79
|
+
});
|
|
80
|
+
return { generation: request.generation, errors: Object.fromEntries(failed), crashed: true, crashedNeeds };
|
|
93
81
|
}
|
|
94
82
|
}
|