@akanjs/devkit 3.0.0-beta.19 → 3.0.0-beta.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/abstractDoc.test.ts +6 -15
- package/abstractDoc.ts +1 -4
- package/agentsIndex.test.ts +4 -7
- package/agentsIndex.ts +6 -22
- package/akanApp/BackendImportGraph.test.ts +8 -22
- package/akanApp/BackendImportGraph.ts +1 -20
- package/akanApp/akanApp.host.test.ts +1 -3
- package/akanApp/akanApp.host.ts +64 -250
- package/akanApp/devHostPolicy.test.ts +10 -34
- package/akanApp/devHostPolicy.ts +12 -98
- package/akanConfig/akanConfig.test.ts +30 -39
- package/akanConfig/akanConfig.ts +40 -136
- package/akanContext.ts +106 -159
- package/akanMcpContract.ts +28 -73
- package/appSelectionMemory.ts +1 -4
- package/applicationBuildReporter.ts +37 -5
- package/applicationBuildRunner.ts +25 -59
- package/applicationReleasePackager.ts +24 -44
- package/applicationTestPreload.ts +3 -12
- package/artifact/implicitRootLayout.test.ts +3 -19
- package/artifact/implicitRootLayout.ts +34 -91
- package/artifact/routeSeedIndex.ts +11 -44
- package/biomeBase.ts +2 -5
- package/capacitorApp.test.ts +4 -14
- package/capacitorApp.ts +86 -235
- package/cloud/cloudApi.test.ts +202 -0
- package/cloud/cloudApi.ts +33 -57
- package/cloud/configLock.test.ts +64 -0
- package/cloud/configLock.ts +96 -0
- package/cloud/constants.ts +3 -2
- package/cloud/globalConfig.ts +64 -31
- package/codeAgent/agent/AkanCodeServices.ts +15 -57
- package/codeAgent/agent/AkanContextFiles.ts +5 -28
- package/codeAgent/agent/CodeAgent.ts +33 -108
- package/codeAgent/agent/CodeAgentAsks.ts +7 -30
- package/codeAgent/agent/CodeAgentEventMapper.ts +9 -52
- package/codeAgent/agent/CodeAgentGate.ts +8 -34
- package/codeAgent/agent/CodeAgentProxy.ts +4 -13
- package/codeAgent/agent/CodeAgentRpcHost.ts +3 -16
- package/codeAgent/agent/CodeAgentRpcListener.ts +2 -10
- package/codeAgent/agent/CodeAgentStreamPrinter.ts +4 -15
- package/codeAgent/agent/CodeAgentSuspended.ts +1 -8
- package/codeAgent/agent/CodeAgentUi.ts +2 -7
- package/codeAgent/agent/CodeMailbox.ts +7 -34
- package/codeAgent/agent/CodeSessionFork.ts +4 -19
- package/codeAgent/agent/CodeSessionIndex.ts +5 -31
- package/codeAgent/agent/SubagentPool.ts +48 -58
- package/codeAgent/agent/akanCodeModel.ts +5 -39
- package/codeAgent/agent/akanCodePaths.ts +4 -24
- package/codeAgent/agent/akanContextFiles.test.ts +0 -2
- package/codeAgent/agent/akanSystemPrompt.ts +1 -9
- package/codeAgent/agent/codeAgent.test.ts +178 -0
- package/codeAgent/agent/codeAgentContract.test.ts +5 -28
- package/codeAgent/agent/codeMailbox.test.ts +0 -4
- package/codeAgent/agent/codeSessionFork.test.ts +0 -2
- package/codeAgent/agent/codeSessionIndex.test.ts +9 -4
- package/codeAgent/agent/consoleToStderr.ts +2 -12
- package/codeAgent/agent/jsonLines.ts +29 -0
- package/codeAgent/agent/stringArg.ts +5 -0
- package/codeAgent/feedback/DevLogFeedback.ts +3 -18
- package/codeAgent/feedback/PreviewChrome.ts +5 -29
- package/codeAgent/feedback/PreviewFeedback.ts +2 -9
- package/codeAgent/feedback/PreviewView.ts +3 -15
- package/codeAgent/feedback/TurnFeedback.ts +2 -17
- package/codeAgent/feedback/VerifyFeedback.ts +1 -7
- package/codeAgent/feedback/previewChrome.test.ts +1 -1
- package/codeAgent/index.ts +2 -10
- package/codeAgent/skills/akan-code/SKILL.md +12 -4
- package/codeAgent/tools/AkanCli.ts +4 -16
- package/codeAgent/tools/AkanCodePlugins.ts +21 -43
- package/codeAgent/tools/AkanEditScope.ts +2 -12
- package/codeAgent/tools/AkanEnvKeys.ts +3 -11
- package/codeAgent/tools/AkanToolPack.ts +6 -28
- package/codeAgent/tools/AkanVerifier.ts +1 -8
- package/codeAgent/tools/AskToolPack.ts +3 -15
- package/codeAgent/tools/MailToolPack.ts +0 -9
- package/codeAgent/tools/McpClient.ts +3 -24
- package/codeAgent/tools/McpOAuth.ts +6 -41
- package/codeAgent/tools/McpOAuthLoopback.ts +2 -15
- package/codeAgent/tools/McpServerConfig.ts +7 -26
- package/codeAgent/tools/McpSignIn.ts +46 -49
- package/codeAgent/tools/McpTokenStore.ts +6 -17
- package/codeAgent/tools/McpToolPack.ts +107 -35
- package/codeAgent/tools/SessionToolPack.ts +1 -11
- package/codeAgent/tools/WebToolPack.ts +1 -7
- package/codeAgent/tools/akanCodePlugins.test.ts +231 -63
- package/codeAgent/tools/askToolPack.test.ts +0 -3
- package/codeAgent/tools/mcpOAuth.test.ts +315 -27
- package/codeAgent/tools/mcpServerConfig.test.ts +21 -14
- package/codegenLock.test.ts +4 -13
- package/codegenLock.ts +3 -17
- package/commandDecorators/argMeta.ts +1 -5
- package/commandDecorators/camelToKebabCase.ts +1 -0
- package/commandDecorators/command.ts +44 -102
- package/commandDecorators/commandBuilder.ts +30 -61
- package/commandDecorators/commandDecorators.test.ts +1 -9
- package/commandDecorators/dependencyBuilder.ts +10 -1
- package/commandDecorators/helpFormatter.ts +66 -144
- package/commandDecorators/index.ts +0 -2
- package/commandDecorators/targetMeta.ts +9 -7
- package/createTunnel.ts +7 -14
- package/dependencyScanner.test.ts +4 -16
- package/dependencyScanner.ts +27 -78
- package/devkitUtils.test.ts +47 -19
- package/executors.test.ts +114 -60
- package/executors.ts +165 -484
- package/fileEditor.ts +9 -37
- package/{getDirname.test.ts → fileSys.test.ts} +1 -1
- package/fileSys.ts +6 -8
- package/formSetterScanner.ts +3 -16
- package/frontendBuild/allRoutesBuilder.ts +7 -21
- package/frontendBuild/autoImportSync.test.ts +14 -38
- package/frontendBuild/autoImportSync.ts +15 -67
- package/frontendBuild/buildRouteClient.test.ts +10 -36
- package/frontendBuild/bundleDefine.ts +12 -0
- package/frontendBuild/clientBuildTypes.ts +5 -36
- package/frontendBuild/clientEntriesBundler.ts +9 -40
- package/frontendBuild/clientEntryDiscovery.ts +6 -41
- package/frontendBuild/csrArtifactBuilder.ts +6 -25
- package/frontendBuild/cssCandidateCache.test.ts +0 -6
- package/frontendBuild/cssCandidateCache.ts +6 -41
- package/frontendBuild/cssCompiler.ts +29 -78
- package/frontendBuild/cssImportResolver.ts +5 -18
- package/frontendBuild/devChangePlanner.ts +7 -10
- package/frontendBuild/devGeneratedIndexSync.test.ts +12 -27
- package/frontendBuild/devGeneratedIndexSync.ts +1 -4
- package/frontendBuild/fontOptimizer.test.ts +5 -13
- package/frontendBuild/fontOptimizer.ts +15 -72
- package/frontendBuild/fontPruner.test.ts +5 -10
- package/frontendBuild/fontPruner.ts +5 -20
- package/frontendBuild/frontendBuild.test.ts +27 -52
- package/frontendBuild/hmrChangeClassifier.ts +2 -2
- package/frontendBuild/hmrWatcher.test.ts +7 -31
- package/frontendBuild/hmrWatcher.ts +14 -71
- package/frontendBuild/pagesBundleBuilder.ts +4 -29
- package/frontendBuild/precompressArtifacts.ts +5 -18
- package/frontendBuild/routeClientBuilder.ts +7 -47
- package/frontendBuild/routesManifestArtifactSerializer.ts +5 -36
- package/frontendBuild/sourceMtimeIndex.test.ts +37 -94
- package/frontendBuild/sourceMtimeIndex.ts +19 -130
- package/frontendBuild/ssrBaseArtifactBuilder.ts +2 -8
- package/frontendBuild/styleContract.ts +1 -8
- package/frontendBuild/themeValidator.ts +4 -26
- package/frontendBuild/watchRootResolver.test.ts +23 -42
- package/frontendBuild/watchRootResolver.ts +5 -27
- package/getRelatedCnsts.ts +12 -92
- package/incrementalBuilder/buildBatch.proc.ts +22 -57
- package/incrementalBuilder/buildBatchProtocol.ts +8 -36
- package/incrementalBuilder/buildBatchRunner.test.ts +34 -0
- package/incrementalBuilder/buildBatchRunner.ts +10 -31
- package/incrementalBuilder/builderChannel.test.ts +24 -49
- package/incrementalBuilder/builderChannel.ts +6 -37
- package/incrementalBuilder/builderRequestRouter.test.ts +0 -8
- package/incrementalBuilder/builderRequestRouter.ts +5 -24
- package/incrementalBuilder/devWatchBatch.test.ts +14 -18
- package/incrementalBuilder/incrementalBuilder.host.test.ts +18 -55
- package/incrementalBuilder/incrementalBuilder.host.ts +18 -76
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +81 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +48 -161
- package/index.ts +3 -15
- package/integration/devResourceProbe.ts +50 -56
- package/integration/devStability.integration.test.ts +47 -194
- package/integration/devStabilityHarness.test.ts +9 -29
- package/integration/devStabilityHarness.ts +65 -330
- package/integration/ssrMemoryProbe.ts +41 -167
- package/libSource.test.ts +4 -15
- package/libSource.ts +19 -31
- package/lint/gritRules.test.ts +6 -23
- package/linter.test.ts +1 -2
- package/linter.ts +22 -115
- package/mobile/mobileTarget.ts +14 -13
- package/package.json +2 -2
- package/packageExports.test.ts +3 -18
- package/packageExportsMap.ts +3 -19
- package/piDependencyPins.test.ts +3 -14
- package/platformTest/IdleWatchedProcess.test.ts +5 -13
- package/platformTest/IdleWatchedProcess.ts +1 -5
- package/platformTest/LinuxDockerTarget.ts +20 -35
- package/platformTest/PlatformTestRun.test.ts +5 -9
- package/platformTest/PlatformTestTarget.ts +24 -2
- package/platformTest/TestSnapshot.test.ts +6 -14
- package/platformTest/TestSnapshot.ts +1 -5
- package/platformTest/WindowsSshTarget.ts +6 -22
- package/prompter.ts +3 -6
- package/qualityScanner.test.ts +6 -24
- package/qualityScanner.ts +196 -284
- package/recipeScanner.test.ts +7 -12
- package/recipeScanner.ts +9 -38
- package/repoIdentity.ts +2 -9
- package/routeSourceValidator.ts +7 -30
- package/scanInfo.ts +82 -214
- package/selectModel.ts +1 -2
- package/semver.ts +3 -5
- package/slicePlanner.test.ts +6 -24
- package/slicePlanner.ts +11 -32
- package/spinner.test.ts +3 -8
- package/spinner.ts +4 -16
- package/sshTunnel.ts +1 -6
- package/ssrScanner.test.ts +1 -6
- package/ssrScanner.ts +103 -113
- package/subspace.test.ts +6 -25
- package/subspace.ts +31 -109
- package/subspaceConfig.ts +3 -11
- package/testHelpers.ts +49 -34
- package/transforms/asyncDefaultExportDetector.ts +3 -11
- package/transforms/barrelAnalyzer.ts +43 -135
- package/transforms/barrelImportsPlugin.ts +38 -175
- package/transforms/externalizeFrameworkPlugin.test.ts +39 -118
- package/transforms/externalizeFrameworkPlugin.ts +13 -87
- package/transforms/moduleSyntax.ts +32 -0
- package/transforms/rscUseClientTransform.ts +35 -20
- package/transforms/transforms.test.ts +153 -44
- package/transforms/tsconfigPackageResolver.test.ts +6 -24
- package/transforms/useClientBundlePlugin.ts +2 -22
- package/typeChecker.ts +7 -63
- package/uploadRelease.ts +10 -25
- package/workflow/artifacts.ts +18 -0
- package/workflow/executor.test.ts +1 -12
- package/workflow/executor.ts +85 -146
- package/workflow/index.ts +0 -1
- package/workflow/moduleIndex.test.ts +4 -15
- package/workflow/moduleIndex.ts +9 -49
- package/workflow/plan.ts +18 -25
- package/workflow/render.ts +80 -96
- package/workflow/source.ts +33 -56
- package/workflow/sourceAst.ts +50 -0
- package/workspaceLayout.ts +4 -19
- package/commandDecorators/commandMeta.ts +0 -7
- package/commandDecorators/types.ts +0 -10
- package/getDirname.ts +0 -4
- package/local/optimize-resource/ipcprobe/child.ts +0 -1
- package/local/optimize-resource/ipcprobe/parent.ts +0 -11
- package/workflow/primitive.ts +0 -59
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
// Module paths, never a barrel
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
// (~40MB) into a process that then holds them for the whole dev session. Phase 2 moved css compilation
|
|
6
|
-
// into the batch worker, so this process has no use for them — `entryModuleGraph.test.ts` keeps it that way.
|
|
2
|
+
// Module paths, never a barrel: the devkit and `frontendBuild` barrels would hold tailwindcss (~40MB), ssh2, ink
|
|
3
|
+
// and the cloud stack for the whole dev session (`entryModuleGraph.test.ts` enforces it).
|
|
4
|
+
import { ApplicationBuildReporter } from "@akanjs/devkit/applicationBuildReporter";
|
|
7
5
|
import { CodegenLock } from "@akanjs/devkit/codegenLock";
|
|
8
6
|
import type { App } from "@akanjs/devkit/commandDecorators";
|
|
9
7
|
import { AppExecutor, type PageRoot, WorkspaceExecutor } from "@akanjs/devkit/executors";
|
|
@@ -41,6 +39,7 @@ interface IncrementalBuilderOptions {
|
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
type IncrementalBuilderBootDeps = Pick<IncrementalBuilderOptions, "artifact" | "optimizedFonts" | "discovery">;
|
|
42
|
+
type BatchWork = Pick<BuildBatchRequest, "generation" | "needs" | "changedFiles">;
|
|
44
43
|
|
|
45
44
|
class IncrementalBuilder {
|
|
46
45
|
#logger = new Logger("IncrementalBuilder");
|
|
@@ -84,10 +83,7 @@ class IncrementalBuilder {
|
|
|
84
83
|
return `${this.#app.cwdPath}/.akan/artifact`;
|
|
85
84
|
}
|
|
86
85
|
|
|
87
|
-
|
|
88
|
-
* Build a route and answer it. The reply is part of the work item on purpose: `shutdown` drains the
|
|
89
|
-
* work queue before exiting, so folding the flush in here is what makes "drained" mean "answered".
|
|
90
|
-
*/
|
|
86
|
+
// The reply is part of the work item on purpose: `shutdown` drains the queue, so "drained" must mean "answered".
|
|
91
87
|
async handleBuildRoute(msg: BuilderReq): Promise<void> {
|
|
92
88
|
await this.#enqueueWork(`build-route:${msg.routeId}`, async () =>
|
|
93
89
|
BuilderChannel.send(await this.#handleBuildRoute(msg)),
|
|
@@ -123,7 +119,7 @@ class IncrementalBuilder {
|
|
|
123
119
|
} as BuildRouteResultPayload,
|
|
124
120
|
};
|
|
125
121
|
} catch (err) {
|
|
126
|
-
const errMsg = err
|
|
122
|
+
const errMsg = ApplicationBuildReporter.formatError(err, this.#app.workspace.workspaceRoot);
|
|
127
123
|
this.#logger.error(`build-route failed routeId=${msg.routeId}: ${errMsg}`);
|
|
128
124
|
this.#sendBuildStatus("route", { generation: msg.generation, ok: false, files: msg.seeds, message: errMsg });
|
|
129
125
|
return { type: "build-route-res", id: msg.id, ok: false, error: errMsg };
|
|
@@ -160,16 +156,11 @@ class IncrementalBuilder {
|
|
|
160
156
|
}
|
|
161
157
|
}
|
|
162
158
|
|
|
163
|
-
/** No queued work and no debounced css rebuild, so nothing is lost if the process exits now. */
|
|
164
159
|
get #idle(): boolean {
|
|
165
160
|
return this.#inFlight === 0 && this.#cssRebuildTimer === null;
|
|
166
161
|
}
|
|
167
162
|
|
|
168
|
-
|
|
169
|
-
* Reported only when the builder is idle. The host's only lever against the bundler arenas
|
|
170
|
-
* `Bun.build` retains is to recycle this process, and a recycle decided while work is queued would
|
|
171
|
-
* either truncate that work or race the shutdown drain — so a busy builder simply says nothing.
|
|
172
|
-
*/
|
|
163
|
+
// Idle only: the host recycles on these metrics, and a recycle decided mid-work would truncate it or race the drain.
|
|
173
164
|
#reportMetrics(): void {
|
|
174
165
|
if (!this.#idle || this.#shuttingDown) return;
|
|
175
166
|
BuilderChannel.emit({
|
|
@@ -178,34 +169,24 @@ class IncrementalBuilder {
|
|
|
178
169
|
});
|
|
179
170
|
}
|
|
180
171
|
|
|
181
|
-
|
|
182
|
-
* Finish queued work, then exit so the OS reclaims the bundler arenas. The host restarts a
|
|
183
|
-
* replacement; build requests that arrive during the drain are refused with the same retry error
|
|
184
|
-
* the backend already handles for any other builder restart.
|
|
185
|
-
*/
|
|
172
|
+
// Exits so the OS reclaims the bundler arenas; the host spawns the replacement.
|
|
186
173
|
async shutdown(reason: string): Promise<void> {
|
|
187
174
|
if (this.#shuttingDown) return;
|
|
188
175
|
this.#shuttingDown = true;
|
|
189
176
|
const started = Date.now();
|
|
190
177
|
this.#logger.debug(`shutdown requested (${reason}); draining ${this.#inFlight} work item(s)`);
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
// writing its artifacts. The replacement rebuilds every artifact from its boot build anyway, so the
|
|
194
|
-
// batch is not lost; a half-written one would be.
|
|
178
|
+
// Before the drain: a save landing mid-drain would queue behind the awaited tail and `process.exit` would cut
|
|
179
|
+
// its artifacts off half-written; the replacement's boot build redoes that work anyway.
|
|
195
180
|
this.#watcher?.stop();
|
|
196
181
|
if (this.#cssRebuildTimer) {
|
|
197
|
-
// Only
|
|
198
|
-
// boot build recompiles css from scratch anyway, so dropping the debounce loses nothing.
|
|
182
|
+
// Only if css landed after the idle report; the replacement's boot build recompiles css, so nothing is lost.
|
|
199
183
|
clearTimeout(this.#cssRebuildTimer);
|
|
200
184
|
this.#cssRebuildTimer = null;
|
|
201
185
|
this.#pendingCssRebuild = null;
|
|
202
186
|
}
|
|
203
187
|
await this.#workQueue.catch(() => undefined);
|
|
204
188
|
await this.#cssRebuildQueue.catch(() => undefined);
|
|
205
|
-
// Drained queues
|
|
206
|
-
// largest messages this process sends, and `process.exit` discards an ipc write that has not
|
|
207
|
-
// flushed — a `css-updated` relayed milliseconds before this line would be dropped with no error
|
|
208
|
-
// anywhere, leaving the backend serving the previous bundle. See `BuilderChannel`.
|
|
189
|
+
// Drained queues are not delivered results: `process.exit` drops unflushed ipc writes (a relayed `css-updated`).
|
|
209
190
|
const flushed = await BuilderChannel.drain();
|
|
210
191
|
this.#logger.debug(
|
|
211
192
|
`drained in ${Date.now() - started}ms${flushed ? ` after flushing ${flushed} ipc write(s)` : ""}; exiting for recycle`,
|
|
@@ -216,8 +197,7 @@ class IncrementalBuilder {
|
|
|
216
197
|
get shuttingDown(): boolean {
|
|
217
198
|
return this.#shuttingDown;
|
|
218
199
|
}
|
|
219
|
-
//* Watch events
|
|
220
|
-
//* page key still carries the app-relative `(libs)/(<lib>)` prefix.
|
|
200
|
+
//* Watch events carry real paths: synced lib pages match by `realDir`, their keys keep the `(libs)/(<lib>)` prefix.
|
|
221
201
|
static #matchPageRoot(roots: PageRoot[], abs: string): PageRoot | null {
|
|
222
202
|
for (const root of roots) {
|
|
223
203
|
const absRoot = path.resolve(root.realDir);
|
|
@@ -245,7 +225,6 @@ class IncrementalBuilder {
|
|
|
245
225
|
}
|
|
246
226
|
return false;
|
|
247
227
|
}
|
|
248
|
-
/** Debounced css-only rebuild; the compile itself runs in a disposable worker like every other build. */
|
|
249
228
|
scheduleCssRebuild({ generation, changedFiles }: { generation?: number; changedFiles?: string[] }) {
|
|
250
229
|
this.#pendingCssRebuild = { generation, changedFiles };
|
|
251
230
|
if (this.#cssRebuildTimer) clearTimeout(this.#cssRebuildTimer);
|
|
@@ -280,13 +259,12 @@ class IncrementalBuilder {
|
|
|
280
259
|
}, 150);
|
|
281
260
|
}
|
|
282
261
|
async installWatcher() {
|
|
283
|
-
const artifactDir = this.#artifactDir;
|
|
284
262
|
const roots = await new WatchRootResolver(this.#app).resolve();
|
|
285
263
|
const watcher = new HmrWatcher({
|
|
286
264
|
roots,
|
|
287
265
|
logger: this.#logger,
|
|
288
266
|
onBatch: async (batch: ChangeBatch) => {
|
|
289
|
-
await this.#enqueueWork("hmr-batch", async () => this.#handleWatchBatch(
|
|
267
|
+
await this.#enqueueWork("hmr-batch", async () => this.#handleWatchBatch(batch));
|
|
290
268
|
},
|
|
291
269
|
});
|
|
292
270
|
await watcher.start();
|
|
@@ -294,13 +272,11 @@ class IncrementalBuilder {
|
|
|
294
272
|
this.#logger.verbose(`watching ${roots.length} roots`);
|
|
295
273
|
}
|
|
296
274
|
|
|
297
|
-
async #handleWatchBatch(
|
|
275
|
+
async #handleWatchBatch(batch: ChangeBatch) {
|
|
298
276
|
const rawKinds = new Set(batch.kinds);
|
|
299
277
|
if (rawKinds.size === 0) return;
|
|
300
278
|
const generation = ++this.#generation;
|
|
301
|
-
//*
|
|
302
|
-
//* *.store.ts) before regenerating barrels. Edits land on files already in this batch, so they
|
|
303
|
-
//* rebuild in this same generation; the write is idempotent so it does not re-trigger the watcher.
|
|
279
|
+
//* Auto-import edits touch only this batch's files, so they rebuild in this same generation.
|
|
304
280
|
const [autoImport, indexSync] = await CodegenLock.run(
|
|
305
281
|
this.#app.workspace.workspaceRoot,
|
|
306
282
|
`hmr-batch:${this.#app.name}`,
|
|
@@ -312,9 +288,7 @@ class IncrementalBuilder {
|
|
|
312
288
|
for (const error of autoImport.errors) this.#logger.error(error);
|
|
313
289
|
if (autoImport.changedFiles.length > 0)
|
|
314
290
|
this.#logger.verbose(`[auto-import] inserted imports into ${autoImport.changedFiles.length} file(s)`);
|
|
315
|
-
//*
|
|
316
|
-
//* them to the watcher so its verification scan does not read them back as a user edit and spend a
|
|
317
|
-
//* second generation rebuilding identical content.
|
|
291
|
+
//* Absorbed so the watcher's verification scan does not replay these writes as a user edit next generation.
|
|
318
292
|
await this.#watcher?.absorb([...autoImport.changedFiles, ...indexSync.changedFiles]);
|
|
319
293
|
const { files, kinds, expandedBatch, devPlan, event, hasSyncErrors } = prepareDevWatchBatch({
|
|
320
294
|
generation,
|
|
@@ -343,8 +317,7 @@ class IncrementalBuilder {
|
|
|
343
317
|
}
|
|
344
318
|
if (indexSync.changedFiles.length > 0) this.#sendBuildStatus("barrel", { generation, ok: true, files });
|
|
345
319
|
|
|
346
|
-
// Server-only generations
|
|
347
|
-
// client: a fresh pages buildId would broadcast rsc-refresh to browsers for no visible change.
|
|
320
|
+
// Server-only generations skip the client: a fresh pages buildId would rsc-refresh browsers for no visible change.
|
|
348
321
|
const rebuildClient = devPlan.actions.includes("rebuild-client");
|
|
349
322
|
if (kinds.includes("code") && !rebuildClient) {
|
|
350
323
|
this.#logger.verbose(`client rebuild skipped; devPlan actions=${devPlan.actions.join(",") || "(none)"}`);
|
|
@@ -361,56 +334,36 @@ class IncrementalBuilder {
|
|
|
361
334
|
|
|
362
335
|
const needs: BuildBatchNeed[] = [];
|
|
363
336
|
if (kinds.includes("code") && rebuildClient) {
|
|
364
|
-
if (this.#
|
|
337
|
+
if (this.#csrActive) needs.push("csr");
|
|
365
338
|
else
|
|
366
339
|
this.#logger.verbose(
|
|
367
340
|
`csr-rebundle skipped; request /__csr or ?csr=true (or set AKAN_DEV_CSR_REBUILD=1) to enable per-save CSR rebuilds`,
|
|
368
341
|
);
|
|
369
342
|
needs.push("pages");
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
// generation's batch rather than debounced separately: the work queue already serializes
|
|
373
|
-
// generations, so by the time a second batch runs the 150ms debounce would have fired anyway,
|
|
374
|
-
// and a second worker spawn per save costs more than the coalescing ever saved.
|
|
343
|
+
// In this batch rather than debounced: the queue already serializes generations, and a second worker per save
|
|
344
|
+
// costs more than coalescing would save.
|
|
375
345
|
needs.push("css");
|
|
376
346
|
}
|
|
377
347
|
|
|
378
348
|
BuilderChannel.emit(event);
|
|
379
349
|
|
|
380
350
|
if (needs.length > 0) await this.#runBatch({ generation, needs, changedFiles: files });
|
|
381
|
-
//
|
|
382
|
-
// without a pages build in front of them there is nothing else to space them out.
|
|
351
|
+
// Css-only batches keep the debounce: they arrive in bursts while a stylesheet is edited.
|
|
383
352
|
else if (kinds.includes("css")) {
|
|
384
353
|
this.scheduleCssRebuild({ generation, changedFiles: files });
|
|
385
354
|
this.#logger.verbose(`css-rebuild scheduled generation=${generation}`);
|
|
386
355
|
}
|
|
387
356
|
}
|
|
388
357
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
* messages the backend and the HMR overlay consume, which this relays untouched, so the sequence a
|
|
392
|
-
* browser observes is the same one the in-process build produced.
|
|
393
|
-
*/
|
|
394
|
-
async #runBatch({
|
|
395
|
-
generation,
|
|
396
|
-
needs,
|
|
397
|
-
changedFiles,
|
|
398
|
-
}: {
|
|
399
|
-
generation: number;
|
|
400
|
-
needs: BuildBatchNeed[];
|
|
401
|
-
changedFiles: string[];
|
|
402
|
-
}): Promise<BuildBatchResult> {
|
|
358
|
+
async #runBatch(work: BatchWork): Promise<BuildBatchResult> {
|
|
359
|
+
const { generation, needs, changedFiles } = work;
|
|
403
360
|
const started = Date.now();
|
|
404
|
-
const result = await this.#batchRunner.run(await this.#batchRequest(
|
|
405
|
-
BuilderChannel.emit(msg),
|
|
406
|
-
);
|
|
361
|
+
const result = await this.#batchRunner.run(await this.#batchRequest(work), (msg) => BuilderChannel.emit(msg));
|
|
407
362
|
if (result.optimizedFonts) this.#optimizedFonts = result.optimizedFonts;
|
|
408
363
|
if (result.cssAssets) this.#artifact = { ...this.#artifact, cssAssets: result.cssAssets };
|
|
409
|
-
// A worker
|
|
410
|
-
// it was given: the generation must go red rather than look like it silently succeeded.
|
|
364
|
+
// A crashed worker streamed no build-status, so each need goes red here instead of looking silently successful.
|
|
411
365
|
if (result.crashed) {
|
|
412
|
-
// `base` is
|
|
413
|
-
// it never travels through here — `#buildBootDeps` runs it and throws into the degraded-boot path.
|
|
366
|
+
// `base` is not a `BuildPhase` and never comes through here: `#buildBootDeps` throws into the degraded boot.
|
|
414
367
|
for (const need of needs)
|
|
415
368
|
if (need !== "base")
|
|
416
369
|
this.#sendBuildStatus(need, { generation, ok: false, files: changedFiles, message: result.errors[need] });
|
|
@@ -419,15 +372,7 @@ class IncrementalBuilder {
|
|
|
419
372
|
return result;
|
|
420
373
|
}
|
|
421
374
|
|
|
422
|
-
async #batchRequest({
|
|
423
|
-
generation,
|
|
424
|
-
needs,
|
|
425
|
-
changedFiles,
|
|
426
|
-
}: {
|
|
427
|
-
generation: number;
|
|
428
|
-
needs: BuildBatchNeed[];
|
|
429
|
-
changedFiles: string[];
|
|
430
|
-
}): Promise<BuildBatchRequest> {
|
|
375
|
+
async #batchRequest({ generation, needs, changedFiles }: BatchWork): Promise<BuildBatchRequest> {
|
|
431
376
|
return {
|
|
432
377
|
appName: this.#app.name,
|
|
433
378
|
workspaceRoot: this.#app.workspace.workspaceRoot,
|
|
@@ -448,10 +393,7 @@ class IncrementalBuilder {
|
|
|
448
393
|
this.#logger.verbose(`ready (watch=${this.#watch})`);
|
|
449
394
|
}
|
|
450
395
|
|
|
451
|
-
|
|
452
|
-
* After a degraded boot recovers, the backend is still serving the last-good bundle; push a
|
|
453
|
-
* fresh pages/css state so connected browsers pick up the fixed code without another edit.
|
|
454
|
-
*/
|
|
396
|
+
// The backend still serves the last-good bundle after a degraded boot; push fresh pages/css without another edit.
|
|
455
397
|
async announceRecoveredState(changedFiles: string[]): Promise<void> {
|
|
456
398
|
const generation = ++this.#generation;
|
|
457
399
|
await this.#enqueueWork("boot-recovered", async () => {
|
|
@@ -459,21 +401,12 @@ class IncrementalBuilder {
|
|
|
459
401
|
});
|
|
460
402
|
}
|
|
461
403
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
*
|
|
465
|
-
* The backend reads `base-artifact.json` once at boot and never re-reads it, so a pages or css hash
|
|
466
|
-
* that moved while the builder was being replaced would otherwise leave it pointing at the previous
|
|
467
|
-
* artifact until the next save. Announced from the boot artifact rather than by rebuilding: the
|
|
468
|
-
* bundles are already on disk, and spending another ~200MB of bundler arena in a process that was
|
|
469
|
-
* just recycled to reclaim memory would defeat the purpose. The host suppresses the announcement
|
|
470
|
-
* when the hashes match, which is the common case, so a clean recycle never reloads a browser.
|
|
471
|
-
*/
|
|
404
|
+
// The backend reads base-artifact.json once, at its boot. Re-announced from disk rather than rebuilt (this process
|
|
405
|
+
// was recycled to free bundler memory); the host drops it when hashes match, so a clean recycle reloads nothing.
|
|
472
406
|
async announceBootState(): Promise<void> {
|
|
473
407
|
const generation = ++this.#generation;
|
|
474
408
|
const reason = "builder-recycle" as const;
|
|
475
|
-
// Awaited
|
|
476
|
-
// down at any moment, and "announced boot state" must mean the announcement left the process.
|
|
409
|
+
// Awaited, not emitted: the host may shut this builder down any moment, so "announced" must mean flushed.
|
|
477
410
|
await BuilderChannel.send({
|
|
478
411
|
type: "pages-updated",
|
|
479
412
|
data: {
|
|
@@ -500,16 +433,11 @@ class IncrementalBuilder {
|
|
|
500
433
|
this.#logger.verbose(`announced boot state after recycle generation=${generation}`);
|
|
501
434
|
}
|
|
502
435
|
|
|
503
|
-
|
|
504
|
-
* Build the dev CSR artifact because a request asked for it, and keep it in sync from now on. The
|
|
505
|
-
* dev server only serves CSR through the opt-in `/__csr` and `?csr=true` routes — mobile local dev
|
|
506
|
-
* points a device WebView at the latter — so nothing needs the artifact until one of them is hit.
|
|
507
|
-
*/
|
|
436
|
+
// On demand: dev serves CSR only via the opt-in `/__csr` and `?csr=true` routes; once built, it rebuilds every save.
|
|
508
437
|
async handleBuildCsr(msg: BuilderCsrReq): Promise<void> {
|
|
509
438
|
await this.#enqueueWork("build-csr", async (): Promise<void> => {
|
|
510
439
|
const started = Date.now();
|
|
511
|
-
//
|
|
512
|
-
// never carried a csr status for it before. The error travels in the response below.
|
|
440
|
+
// Not relayed: an on-demand CSR build is request/response, and its error travels in the response.
|
|
513
441
|
const result = await this.#batchRunner.run(
|
|
514
442
|
await this.#batchRequest({ generation: this.#generation, needs: ["csr"], changedFiles: [] }),
|
|
515
443
|
);
|
|
@@ -525,26 +453,11 @@ class IncrementalBuilder {
|
|
|
525
453
|
});
|
|
526
454
|
}
|
|
527
455
|
|
|
528
|
-
#shouldRebuildCsr() {
|
|
529
|
-
return this.#csrActive;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
456
|
static #csrArmedByEnv() {
|
|
533
457
|
return process.env.AKAN_DEV_CSR_REBUILD === "1";
|
|
534
458
|
}
|
|
535
459
|
|
|
536
|
-
|
|
537
|
-
* Build the boot artifact in a process that exits afterwards, and keep only the serializable result.
|
|
538
|
-
*
|
|
539
|
-
* This runs in a worker for the same reason every other build does, and it was the largest single
|
|
540
|
-
* holdout: measured on `apps/akan`, `SsrBaseArtifactBuilder.build()` retains **+1143 MB** that
|
|
541
|
-
* `Bun.gc(true)` cannot touch, which is 65 % of the builder's post-boot RSS. Nothing was lost by
|
|
542
|
-
* moving it — the builder only ever kept `artifact` and `optimizedFonts`, both plain data, and the
|
|
543
|
-
* artifact is written to `base-artifact.json` regardless.
|
|
544
|
-
*
|
|
545
|
-
* `GraphClientEntryDiscovery.create` stays here because route builds need it live, and it costs
|
|
546
|
-
* nothing to keep: measured at **0 ms and 0 MB**, because it builds its graph lazily on first use.
|
|
547
|
-
*/
|
|
460
|
+
// Discovery stays in-process because route builds need it live; it is free at boot, building its graph lazily.
|
|
548
461
|
static async #buildBootDeps(app: App, runner: BuildBatchRunner): Promise<IncrementalBuilderBootDeps> {
|
|
549
462
|
const result = await runner.run({
|
|
550
463
|
appName: app.name,
|
|
@@ -553,15 +466,12 @@ class IncrementalBuilder {
|
|
|
553
466
|
generation: 0,
|
|
554
467
|
needs: ["base"],
|
|
555
468
|
changedFiles: [],
|
|
556
|
-
// Discovered by the worker: at boot the watcher has no validated keys to seed, and the boot build
|
|
557
|
-
// globs them itself anyway.
|
|
558
469
|
pageKeys: null,
|
|
559
470
|
optimizedFonts: null,
|
|
560
471
|
cssAssets: null,
|
|
561
472
|
artifactDir: path.resolve(`${app.cwdPath}/.akan/artifact`),
|
|
562
473
|
});
|
|
563
|
-
//
|
|
564
|
-
// watch mode that keeps the dev server alive until the error is fixed.
|
|
474
|
+
// Throws rather than degrading quietly: `main` catches it to enter degraded watch mode.
|
|
565
475
|
if (result.errors.base) throw new Error(result.errors.base);
|
|
566
476
|
if (!result.artifact || !result.optimizedFonts)
|
|
567
477
|
throw new Error("boot build reported success without an artifact; the build worker likely died");
|
|
@@ -569,13 +479,7 @@ class IncrementalBuilder {
|
|
|
569
479
|
return { artifact: result.artifact, optimizedFonts: result.optimizedFonts, discovery };
|
|
570
480
|
}
|
|
571
481
|
|
|
572
|
-
|
|
573
|
-
* A session that already armed dev CSR keeps it armed across builder restarts through the env flag,
|
|
574
|
-
* so the artifact has to be rebuilt for the replacement. Queued after `builder-ready` and run in a
|
|
575
|
-
* disposable worker rather than inline: a full minified browser-target build of every page costs
|
|
576
|
-
* ~350MB of bundler arena that an inline build would never give back, and nothing serves CSR until a
|
|
577
|
-
* `/__csr` or `?csr=true` request arrives anyway.
|
|
578
|
-
*/
|
|
482
|
+
// The env flag carries an armed CSR session across builder restarts, so the replacement rebuilds it after ready.
|
|
579
483
|
async rearmCsrFromEnv(): Promise<void> {
|
|
580
484
|
if (!IncrementalBuilder.#csrArmedByEnv()) return;
|
|
581
485
|
this.#csrActive = true;
|
|
@@ -588,12 +492,8 @@ class IncrementalBuilder {
|
|
|
588
492
|
});
|
|
589
493
|
}
|
|
590
494
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
* file watcher, so exiting here leaves nothing to notice the fix. Report the failure, emit
|
|
594
|
-
* builder-ready so the host keeps the backend serving the last-good artifact, then retry the
|
|
595
|
-
* boot build on every file change until it succeeds.
|
|
596
|
-
*/
|
|
495
|
+
// A boot compile error must not kill the builder, the dev server's only file watcher: report it, emit builder-ready
|
|
496
|
+
// so the backend keeps the last-good artifact, and retry the boot build on every change.
|
|
597
497
|
static #recoverBoot(
|
|
598
498
|
app: App,
|
|
599
499
|
bootError: unknown,
|
|
@@ -649,8 +549,7 @@ class IncrementalBuilder {
|
|
|
649
549
|
const app = AppExecutor.from(workspace, appName);
|
|
650
550
|
const watch = process.env.AKAN_WATCH !== "0";
|
|
651
551
|
let builder: IncrementalBuilder | null = null;
|
|
652
|
-
// Registered before the boot build so
|
|
653
|
-
// the backend) while the builder is still booting or recovering from a failed build.
|
|
552
|
+
// Registered before the boot build so a request during boot or recovery gets an error instead of hanging.
|
|
654
553
|
const bootingError = "builder is recovering from a failed boot build; retry after the build error is fixed";
|
|
655
554
|
const recyclingError = "builder is recycling to release bundler memory; retry after it restarts";
|
|
656
555
|
process.on("message", (msg: BuilderMessage) => {
|
|
@@ -663,34 +562,21 @@ class IncrementalBuilder {
|
|
|
663
562
|
void builder.shutdown(msg.reason);
|
|
664
563
|
return;
|
|
665
564
|
}
|
|
666
|
-
if (msg.type
|
|
565
|
+
if (msg.type !== "build-route" && msg.type !== "build-csr") return;
|
|
566
|
+
if (!builder || builder.shuttingDown) {
|
|
667
567
|
const error = builder?.shuttingDown ? recyclingError : bootingError;
|
|
668
|
-
|
|
669
|
-
BuilderChannel.emit({ type: "build-route-res", id: msg.id, ok: false, error });
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
void builder.handleBuildRoute(msg);
|
|
568
|
+
BuilderChannel.emit({ type: `${msg.type}-res`, id: msg.id, ok: false, error });
|
|
673
569
|
return;
|
|
674
570
|
}
|
|
675
|
-
if (msg.type === "build-
|
|
676
|
-
|
|
677
|
-
if (!builder || builder.shuttingDown) {
|
|
678
|
-
BuilderChannel.emit({ type: "build-csr-res", id: msg.id, ok: false, error });
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
void builder.handleBuildCsr(msg);
|
|
682
|
-
}
|
|
571
|
+
if (msg.type === "build-route") void builder.handleBuildRoute(msg);
|
|
572
|
+
else void builder.handleBuildCsr(msg);
|
|
683
573
|
});
|
|
684
|
-
//
|
|
685
|
-
// as an orphaned watcher that keeps rebuilding for nobody. Nothing is drained here on purpose —
|
|
686
|
-
// there is no longer anyone on the other end to flush to.
|
|
574
|
+
// Closes when the host dies (even by SIGKILL): nothing left to drain, and an orphan would rebuild for nobody.
|
|
687
575
|
process.on("disconnect", () => {
|
|
688
576
|
logger.warn("host IPC channel closed; exiting builder");
|
|
689
577
|
process.exit(0);
|
|
690
578
|
});
|
|
691
579
|
let recoveredFiles: string[] | null = null;
|
|
692
|
-
// Owned by `main` rather than the instance: the boot build has to run before an instance exists, and
|
|
693
|
-
// a degraded boot re-runs it once per file change until it succeeds.
|
|
694
580
|
const bootRunner = new BuildBatchRunner({ workspaceRoot, cwd: app.cwdPath });
|
|
695
581
|
try {
|
|
696
582
|
builder = new IncrementalBuilder({ app, watch, ...(await IncrementalBuilder.#buildBootDeps(app, bootRunner)) });
|
|
@@ -707,7 +593,8 @@ class IncrementalBuilder {
|
|
|
707
593
|
}
|
|
708
594
|
}
|
|
709
595
|
|
|
710
|
-
|
|
596
|
+
// Awaited, not voided: on Windows Bun exits during a missing file's `Bun.file` read unless an entry await is pending.
|
|
597
|
+
await IncrementalBuilder.main().catch((err) => {
|
|
711
598
|
console.error(err);
|
|
712
599
|
process.exit(1);
|
|
713
600
|
});
|
package/index.ts
CHANGED
|
@@ -1,17 +1,6 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// ssh2, @trapezedev/project and the cloud stack into every process that registered a CLI
|
|
5
|
-
// command, and it is the single largest regression this package can suffer from a one-line edit.
|
|
6
|
-
//
|
|
7
|
-
// `export type *` erases at runtime (verified on Bun 1.3.14: the re-exported module is never loaded),
|
|
8
|
-
// so `import type { App } from "@akanjs/devkit"` stays free and readable while a value import fails to
|
|
9
|
-
// compile with a self-explaining error:
|
|
10
|
-
//
|
|
11
|
-
// TS1362: 'FileSys' cannot be used as a value because it was exported using 'export type'.
|
|
12
|
-
//
|
|
13
|
-
// Import values from the owning facet instead: `import { FileSys } from "@akanjs/devkit/fileSys"`.
|
|
14
|
-
// Keep this file type-only; do not "fix" a TS1362 by widening an entry back to `export *`.
|
|
1
|
+
// XXX: types only. A value import from this barrel loads every facet (ink, typescript, ssh2, …) into the process;
|
|
2
|
+
// `export type *` erases at runtime. Import values from the owning facet (`@akanjs/devkit/fileSys`) and never "fix" a
|
|
3
|
+
// TS1362 by widening an entry back to `export *`.
|
|
15
4
|
export type * from "./akanApp";
|
|
16
5
|
export type * from "./akanConfig";
|
|
17
6
|
export type * from "./akanContext";
|
|
@@ -30,7 +19,6 @@ export type * from "./dependencyScanner";
|
|
|
30
19
|
export type * from "./executors";
|
|
31
20
|
export type * from "./fileSys";
|
|
32
21
|
export type * from "./frontendBuild";
|
|
33
|
-
export type * from "./getDirname";
|
|
34
22
|
export type * from "./getRelatedCnsts";
|
|
35
23
|
export type * from "./incrementalBuilder";
|
|
36
24
|
export type * from "./mobile";
|