@akanjs/devkit 2.3.6-rc.0 → 2.3.6-rc.1
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/akanApp/akanApp.host.test.ts +9 -0
- package/akanApp/akanApp.host.ts +49 -0
- package/frontendBuild/devChangePlanner.ts +12 -0
- package/frontendBuild/frontendBuild.test.ts +21 -0
- package/integration/devStability.integration.test.ts +35 -1
- package/integration/devStabilityHarness.ts +44 -3
- package/package.json +2 -2
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
shouldQueueBuildStatusReplay,
|
|
11
11
|
shouldReplaceLastGoodMessage,
|
|
12
12
|
shouldRestartBackendByDevPlan,
|
|
13
|
+
shouldRestartBuilderByDevPlan,
|
|
13
14
|
shouldRestartDevHostByDevPlan,
|
|
14
15
|
} from "./akanApp.host";
|
|
15
16
|
|
|
@@ -37,6 +38,10 @@ describe("shouldRestartBackendByDevPlan", () => {
|
|
|
37
38
|
expect(shouldRestartBackendByDevPlan(invalidateWithActions(["restart-backend", "report-error"]))).toBe(false);
|
|
38
39
|
});
|
|
39
40
|
|
|
41
|
+
test("does not use backend-only restart when builder recycle is required", () => {
|
|
42
|
+
expect(shouldRestartBackendByDevPlan(invalidateWithActions(["restart-backend", "restart-builder"]))).toBe(false);
|
|
43
|
+
});
|
|
44
|
+
|
|
40
45
|
test("falls back when no devPlan is present", () => {
|
|
41
46
|
expect(
|
|
42
47
|
shouldRestartBackendByDevPlan({
|
|
@@ -49,6 +54,10 @@ describe("shouldRestartBackendByDevPlan", () => {
|
|
|
49
54
|
});
|
|
50
55
|
|
|
51
56
|
describe("shouldRestartDevHostByDevPlan", () => {
|
|
57
|
+
test("detects explicit restart-builder actions", () => {
|
|
58
|
+
expect(shouldRestartBuilderByDevPlan(invalidateWithActions(["restart-builder"]))).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
52
61
|
test("detects explicit restart-dev-host actions", () => {
|
|
53
62
|
expect(shouldRestartDevHostByDevPlan(invalidateWithActions(["restart-dev-host"]))).toBe(true);
|
|
54
63
|
});
|
package/akanApp/akanApp.host.ts
CHANGED
|
@@ -31,9 +31,13 @@ export const shouldRestartBackendByDevPlan = (
|
|
|
31
31
|
): boolean | null => {
|
|
32
32
|
if (!message.devPlan) return null;
|
|
33
33
|
if (message.devPlan.actions.includes("report-error")) return false;
|
|
34
|
+
if (message.devPlan.actions.includes("restart-builder")) return false;
|
|
34
35
|
return message.devPlan.actions.includes("restart-backend");
|
|
35
36
|
};
|
|
36
37
|
|
|
38
|
+
export const shouldRestartBuilderByDevPlan = (message: Extract<BuilderMessage, { type: "invalidate" }>): boolean =>
|
|
39
|
+
message.devPlan?.actions.includes("restart-builder") ?? false;
|
|
40
|
+
|
|
37
41
|
export const shouldRestartDevHostByDevPlan = (message: Extract<BuilderMessage, { type: "invalidate" }>): boolean =>
|
|
38
42
|
message.devPlan?.actions.includes("restart-dev-host") ?? message.kinds.includes("config");
|
|
39
43
|
|
|
@@ -508,6 +512,14 @@ export class AkanAppHost {
|
|
|
508
512
|
this.#sendToBackend(message);
|
|
509
513
|
}
|
|
510
514
|
async #handleInvalidate(message: Extract<BuilderMessage, { type: "invalidate" }>) {
|
|
515
|
+
if (shouldRestartBuilderByDevPlan(message)) {
|
|
516
|
+
try {
|
|
517
|
+
await this.#restartDevChildren(message);
|
|
518
|
+
} catch (err) {
|
|
519
|
+
this.#recordDevHostRestartFailure(message, err);
|
|
520
|
+
}
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
511
523
|
if (shouldRestartDevHostByDevPlan(message)) {
|
|
512
524
|
this.#recordDevHostRestartRequired(message);
|
|
513
525
|
return;
|
|
@@ -518,6 +530,29 @@ export class AkanAppHost {
|
|
|
518
530
|
}
|
|
519
531
|
this.#sendToBackend(message);
|
|
520
532
|
}
|
|
533
|
+
async #restartDevChildren(message: Extract<BuilderMessage, { type: "invalidate" }>): Promise<void> {
|
|
534
|
+
const generation = message.devPlan?.generation ?? message.generation;
|
|
535
|
+
this.logger.warn(
|
|
536
|
+
`[dev-host] recycling builder/backend for runtime metadata generation=${generation ?? "(unknown)"} files=${message.files.length}`,
|
|
537
|
+
);
|
|
538
|
+
if (this.#restartTimer) {
|
|
539
|
+
clearTimeout(this.#restartTimer);
|
|
540
|
+
this.#restartTimer = null;
|
|
541
|
+
}
|
|
542
|
+
if (this.#backendRecoveryTimer) {
|
|
543
|
+
clearTimeout(this.#backendRecoveryTimer);
|
|
544
|
+
this.#backendRecoveryTimer = null;
|
|
545
|
+
}
|
|
546
|
+
this.#pendingRestartReason = null;
|
|
547
|
+
this.#lastGoodFrontend = {};
|
|
548
|
+
this.#buildStatusByPhase.clear();
|
|
549
|
+
this.#pendingBuildStatusReplay = [];
|
|
550
|
+
await this.#stopBackend();
|
|
551
|
+
this.#stopBuilder();
|
|
552
|
+
await this.#backendGraph.refresh();
|
|
553
|
+
await this.#startBuilder();
|
|
554
|
+
this.#startBackend({ generation, files: message.files });
|
|
555
|
+
}
|
|
521
556
|
#recordLastGood(
|
|
522
557
|
message: Extract<BuilderMessage, { type: "pages-updated" }> | Extract<BuilderMessage, { type: "css-updated" }>,
|
|
523
558
|
): void {
|
|
@@ -553,6 +588,20 @@ export class AkanAppHost {
|
|
|
553
588
|
this.#sendOrQueueBuildStatus(status);
|
|
554
589
|
}
|
|
555
590
|
}
|
|
591
|
+
#recordDevHostRestartFailure(message: Extract<BuilderMessage, { type: "invalidate" }>, err: unknown): void {
|
|
592
|
+
const generation = message.devPlan?.generation ?? message.generation ?? this.#nextBackendBuildStatusGeneration();
|
|
593
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
594
|
+
this.logger.warn(`[dev-host] runtime metadata restart failed generation=${generation}: ${detail}`);
|
|
595
|
+
const status: DevBuildStatus = {
|
|
596
|
+
generation,
|
|
597
|
+
phase: "scan",
|
|
598
|
+
ok: false,
|
|
599
|
+
files: message.files,
|
|
600
|
+
message: `Runtime metadata change requires restarting \`akan start\` to apply: ${detail}`,
|
|
601
|
+
};
|
|
602
|
+
this.#recordBuildStatus(status);
|
|
603
|
+
this.#sendOrQueueBuildStatus(status);
|
|
604
|
+
}
|
|
556
605
|
#recordBuildStatus(status: DevBuildStatus): void {
|
|
557
606
|
const recovered = shouldMarkBuildPhaseRecovered(this.#buildStatusByPhase, status);
|
|
558
607
|
this.#buildStatusByPhase.set(status.phase, status);
|
|
@@ -50,6 +50,7 @@ export class DevChangePlanner {
|
|
|
50
50
|
const reasons = new Set<string>();
|
|
51
51
|
const fileRoles = this.#rolesForFile(file, { isGenerated: generatedSet.has(path.resolve(file)), reasons });
|
|
52
52
|
for (const role of fileRoles) roles.add(role);
|
|
53
|
+
if (reasons.has("runtime-metadata")) actions.add("restart-builder");
|
|
53
54
|
if (reasons.size > 0) reasonByFile[path.resolve(file)] = [...reasons].sort();
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -104,6 +105,9 @@ export class DevChangePlanner {
|
|
|
104
105
|
roles.add("shared");
|
|
105
106
|
reasons.add("shared-path");
|
|
106
107
|
}
|
|
108
|
+
if (isSource && this.#isRuntimeMetadataFile(parts, base)) {
|
|
109
|
+
reasons.add("runtime-metadata");
|
|
110
|
+
}
|
|
107
111
|
|
|
108
112
|
if (roles.has("server") && roles.has("client")) {
|
|
109
113
|
roles.delete("server");
|
|
@@ -148,6 +152,14 @@ export class DevChangePlanner {
|
|
|
148
152
|
);
|
|
149
153
|
}
|
|
150
154
|
|
|
155
|
+
#isRuntimeMetadataFile(parts: string[], base: string): boolean {
|
|
156
|
+
const parent = parts.at(-2);
|
|
157
|
+
if (parent === "lib" && RUNTIME_METADATA_BASENAMES.has(base)) return true;
|
|
158
|
+
const libIndex = parts.lastIndexOf("lib");
|
|
159
|
+
if (libIndex < 0 || parts.length <= libIndex + 1) return false;
|
|
160
|
+
return base.endsWith(".dictionary.ts") || base.endsWith(".signal.ts");
|
|
161
|
+
}
|
|
162
|
+
|
|
151
163
|
#isBarrelFacetChild(parts: string[]): boolean {
|
|
152
164
|
if (parts.length < 4) return false;
|
|
153
165
|
const [scope, , facet, child] = parts;
|
|
@@ -338,6 +338,27 @@ describe("DevChangePlanner", () => {
|
|
|
338
338
|
planner.plan({ generation: 2, files: [`${root}/apps/akan/akan.config.ts`], kinds: ["config"] }).actions,
|
|
339
339
|
).toEqual(["restart-dev-host"]);
|
|
340
340
|
});
|
|
341
|
+
|
|
342
|
+
test("recycles builder for macro-backed dictionary and signal metadata changes", () => {
|
|
343
|
+
const root = "/repo";
|
|
344
|
+
const planner = new DevChangePlanner({ workspaceRoot: root });
|
|
345
|
+
const dictionaryPlan = planner.plan({
|
|
346
|
+
generation: 3,
|
|
347
|
+
files: [`${root}/apps/demo/lib/_demo/demo.dictionary.ts`],
|
|
348
|
+
kinds: ["code"],
|
|
349
|
+
});
|
|
350
|
+
const signalPlan = planner.plan({
|
|
351
|
+
generation: 4,
|
|
352
|
+
files: [`${root}/libs/shared/lib/admin/admin.signal.ts`],
|
|
353
|
+
kinds: ["code"],
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
expect(dictionaryPlan.actions).toEqual(["rebuild-client", "restart-backend", "restart-builder"]);
|
|
357
|
+
expect(dictionaryPlan.reasonByFile[`${root}/apps/demo/lib/_demo/demo.dictionary.ts`]).toContain(
|
|
358
|
+
"runtime-metadata",
|
|
359
|
+
);
|
|
360
|
+
expect(signalPlan.actions).toContain("restart-builder");
|
|
361
|
+
});
|
|
341
362
|
});
|
|
342
363
|
|
|
343
364
|
describe("CssImportResolver", () => {
|
|
@@ -3,7 +3,7 @@ import { DevGeneratedIndexSync } from "../frontendBuild";
|
|
|
3
3
|
import { DevStabilityHarness } from "./devStabilityHarness";
|
|
4
4
|
|
|
5
5
|
const integrationEnabled = process.env.AKAN_DEV_STABILITY_INTEGRATION === "1";
|
|
6
|
-
const INTEGRATION_TIMEOUT_MS =
|
|
6
|
+
const INTEGRATION_TIMEOUT_MS = 120_000;
|
|
7
7
|
const harnesses: DevStabilityHarness[] = [];
|
|
8
8
|
|
|
9
9
|
const integrationTest = (name: string, fn: () => Promise<void>): void => {
|
|
@@ -125,6 +125,40 @@ describe("dev stability integration harness", () => {
|
|
|
125
125
|
hmr?.close();
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
integrationTest("dictionary edits recycle runtime metadata and replace stale snapshots", async () => {
|
|
129
|
+
const harness = await createHarness();
|
|
130
|
+
const host = await harness.startHost();
|
|
131
|
+
const hmr = await harness.tryConnectHmrProbe();
|
|
132
|
+
const initialHtml = await harness.tryWaitForHttpText("initial-shared-marker", 3_000);
|
|
133
|
+
if (!initialHtml) {
|
|
134
|
+
expect(host.proc.killed).toBe(false);
|
|
135
|
+
hmr?.close();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const mark = host.markLog();
|
|
139
|
+
|
|
140
|
+
await harness.writeFile(
|
|
141
|
+
"lib/_fixture/fixture.dictionary.ts",
|
|
142
|
+
`import { serviceDictionary } from "akanjs/dictionary";
|
|
143
|
+
|
|
144
|
+
import type { FixtureEndpoint } from "./fixture.signal";
|
|
145
|
+
|
|
146
|
+
export const dictionary = serviceDictionary(["en", "ko"])
|
|
147
|
+
.endpoint<FixtureEndpoint>(() => ({}))
|
|
148
|
+
.translate({
|
|
149
|
+
hello: ["Updated Dictionary", "업데이트 사전"],
|
|
150
|
+
});
|
|
151
|
+
`,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
await host.waitForLogSince(mark, /\[dev-plan\].*actions=.*restart-builder/);
|
|
155
|
+
await host.waitForLogSince(mark, /\[dev-host\] recycling builder\/backend for runtime metadata/);
|
|
156
|
+
await host.waitForLogSince(mark, /backend ready pid=(\d+)|AkanApp gateway is running on port/);
|
|
157
|
+
await harness.waitForHttpText("initial-shared-marker");
|
|
158
|
+
expect(host.proc.killed).toBe(false);
|
|
159
|
+
hmr?.close();
|
|
160
|
+
});
|
|
161
|
+
|
|
128
162
|
integrationTest("client build failure reports error and recovers after fix", async () => {
|
|
129
163
|
const harness = await createHarness();
|
|
130
164
|
const host = await harness.startHost();
|
|
@@ -26,7 +26,7 @@ export interface DevStabilityHmrProbe {
|
|
|
26
26
|
close(): void;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
const DEFAULT_TIMEOUT_MS =
|
|
29
|
+
const DEFAULT_TIMEOUT_MS = 60_000;
|
|
30
30
|
|
|
31
31
|
const wait = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
|
|
32
32
|
|
|
@@ -196,6 +196,38 @@ export default function Page() {
|
|
|
196
196
|
this.writeFile(
|
|
197
197
|
"srvkit/backendMarker.ts",
|
|
198
198
|
`export const backendMarker = "initial-backend-marker";
|
|
199
|
+
`,
|
|
200
|
+
),
|
|
201
|
+
this.writeFile(
|
|
202
|
+
"lib/_fixture/fixture.service.ts",
|
|
203
|
+
`import { serve } from "akanjs/service";
|
|
204
|
+
|
|
205
|
+
export class FixtureService extends serve("fixture" as const, { serverMode: "batch" }, () => ({})) {}
|
|
206
|
+
`,
|
|
207
|
+
),
|
|
208
|
+
this.writeFile(
|
|
209
|
+
"lib/_fixture/fixture.signal.ts",
|
|
210
|
+
`import { endpoint, internal } from "akanjs/signal";
|
|
211
|
+
|
|
212
|
+
import * as srv from "../srv";
|
|
213
|
+
|
|
214
|
+
export class FixtureInternal extends internal(srv.fixture, () => ({})) {}
|
|
215
|
+
|
|
216
|
+
export class FixtureEndpoint extends endpoint(srv.fixture, () => ({})) {}
|
|
217
|
+
`,
|
|
218
|
+
),
|
|
219
|
+
this.writeFile(
|
|
220
|
+
"lib/_fixture/fixture.dictionary.ts",
|
|
221
|
+
`import { serviceDictionary } from "akanjs/dictionary";
|
|
222
|
+
|
|
223
|
+
import type { FixtureEndpoint } from "./fixture.signal";
|
|
224
|
+
|
|
225
|
+
export const dictionary = serviceDictionary(["en", "ko"])
|
|
226
|
+
.endpoint<FixtureEndpoint>(() => ({}))
|
|
227
|
+
.translate({
|
|
228
|
+
hello: ["Initial Dictionary", "초기 사전"],
|
|
229
|
+
removeMe: ["Remove Me", "삭제 예정"],
|
|
230
|
+
});
|
|
199
231
|
`,
|
|
200
232
|
),
|
|
201
233
|
this.writeFile(
|
|
@@ -222,7 +254,7 @@ export default function Page() {
|
|
|
222
254
|
|
|
223
255
|
async startHost(timeoutMs = DEFAULT_TIMEOUT_MS): Promise<DevStabilityHost> {
|
|
224
256
|
const logs: string[] = [];
|
|
225
|
-
const proc = Bun.spawn(["
|
|
257
|
+
const proc = Bun.spawn(["bash", "-lc", `bun run akan start ${JSON.stringify(this.appName)}`], {
|
|
226
258
|
cwd: this.workspaceRoot,
|
|
227
259
|
env: {
|
|
228
260
|
...process.env,
|
|
@@ -237,7 +269,16 @@ export default function Page() {
|
|
|
237
269
|
const consume = async (stream: ReadableStream<Uint8Array> | null) => {
|
|
238
270
|
if (!stream) return;
|
|
239
271
|
const decoder = new TextDecoder();
|
|
240
|
-
|
|
272
|
+
const reader = stream.getReader();
|
|
273
|
+
try {
|
|
274
|
+
while (true) {
|
|
275
|
+
const { done, value } = await reader.read();
|
|
276
|
+
if (done) break;
|
|
277
|
+
logs.push(decoder.decode(value, { stream: true }));
|
|
278
|
+
}
|
|
279
|
+
} finally {
|
|
280
|
+
reader.releaseLock();
|
|
281
|
+
}
|
|
241
282
|
};
|
|
242
283
|
void consume(proc.stdout);
|
|
243
284
|
void consume(proc.stderr);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "2.3.6-rc.
|
|
3
|
+
"version": "2.3.6-rc.1",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"@langchain/openai": "^1.4.6",
|
|
33
33
|
"@tailwindcss/node": "^4.3.0",
|
|
34
34
|
"@trapezedev/project": "^7.1.4",
|
|
35
|
-
"akanjs": "2.3.6-rc.
|
|
35
|
+
"akanjs": "2.3.6-rc.1",
|
|
36
36
|
"chalk": "^5.6.2",
|
|
37
37
|
"commander": "^14.0.3",
|
|
38
38
|
"daisyui": "^5.5.20",
|