@akanjs/devkit 2.3.5 → 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/CHANGELOG.md +7 -0
- package/akanApp/akanApp.host.test.ts +211 -0
- package/akanApp/akanApp.host.ts +360 -27
- package/frontendBuild/devChangePlanner.ts +179 -0
- package/frontendBuild/devGeneratedIndexSync.ts +157 -0
- package/frontendBuild/frontendBuild.test.ts +110 -1
- package/frontendBuild/index.ts +2 -0
- package/incrementalBuilder/devWatchBatch.test.ts +59 -0
- package/incrementalBuilder/devWatchBatch.ts +48 -0
- package/incrementalBuilder/incrementalBuilder.host.ts +1 -0
- package/incrementalBuilder/incrementalBuilder.proc.ts +69 -13
- package/incrementalBuilder/index.ts +1 -0
- package/integration/devStability.integration.test.ts +248 -0
- package/integration/devStabilityHarness.ts +485 -0
- package/lint/no-deep-internal-import.grit +2 -2
- package/package.json +2 -2
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
type ClientEntryDiscovery,
|
|
7
7
|
CsrArtifactBuilder,
|
|
8
8
|
type CssCompiler,
|
|
9
|
+
DevChangePlanner,
|
|
10
|
+
DevGeneratedIndexSync,
|
|
9
11
|
FontOptimizer,
|
|
10
12
|
GraphClientEntryDiscovery,
|
|
11
13
|
HmrWatcher,
|
|
@@ -18,12 +20,13 @@ import {
|
|
|
18
20
|
import { Logger } from "akanjs/common";
|
|
19
21
|
import type {
|
|
20
22
|
BaseBuildArtifact,
|
|
21
|
-
BuilderEvent,
|
|
22
23
|
BuilderMessage,
|
|
23
24
|
BuilderReq,
|
|
24
25
|
BuilderRes,
|
|
26
|
+
BuildPhase,
|
|
25
27
|
BuildRouteResultPayload,
|
|
26
28
|
} from "akanjs/server";
|
|
29
|
+
import { prepareDevWatchBatch } from "./devWatchBatch";
|
|
27
30
|
|
|
28
31
|
interface IncrementalBuilderOptions {
|
|
29
32
|
app: App;
|
|
@@ -42,6 +45,8 @@ class IncrementalBuilder {
|
|
|
42
45
|
#cssCompiler: CssCompiler;
|
|
43
46
|
#optimizedFonts: Awaited<ReturnType<FontOptimizer["optimize"]>>;
|
|
44
47
|
#discovery: ClientEntryDiscovery;
|
|
48
|
+
#changePlanner: DevChangePlanner;
|
|
49
|
+
#generatedIndexSync: DevGeneratedIndexSync;
|
|
45
50
|
#generation = 0;
|
|
46
51
|
#workQueue: Promise<void> = Promise.resolve();
|
|
47
52
|
#cssRebuildQueue: Promise<void> = Promise.resolve();
|
|
@@ -55,6 +60,8 @@ class IncrementalBuilder {
|
|
|
55
60
|
this.#cssCompiler = options.cssCompiler;
|
|
56
61
|
this.#optimizedFonts = options.optimizedFonts;
|
|
57
62
|
this.#discovery = options.discovery;
|
|
63
|
+
this.#changePlanner = new DevChangePlanner({ workspaceRoot: options.app.workspace.workspaceRoot });
|
|
64
|
+
this.#generatedIndexSync = new DevGeneratedIndexSync({ workspaceRoot: options.app.workspace.workspaceRoot });
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
async handleBuildRoute(msg: BuilderReq): Promise<BuilderRes> {
|
|
@@ -72,6 +79,7 @@ class IncrementalBuilder {
|
|
|
72
79
|
discovery: this.#discovery,
|
|
73
80
|
}).build();
|
|
74
81
|
this.#logger.verbose(`build-route ok routeId=${msg.routeId} newEntries=${delta.newEntries.length}`);
|
|
82
|
+
this.#sendBuildStatus("route", { generation: msg.generation, ok: true, files: msg.seeds });
|
|
75
83
|
return {
|
|
76
84
|
type: "build-route-res",
|
|
77
85
|
id: msg.id,
|
|
@@ -90,9 +98,26 @@ class IncrementalBuilder {
|
|
|
90
98
|
} catch (err) {
|
|
91
99
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
92
100
|
this.#logger.error(`build-route failed routeId=${msg.routeId}: ${errMsg}`);
|
|
101
|
+
this.#sendBuildStatus("route", { generation: msg.generation, ok: false, files: msg.seeds, message: errMsg });
|
|
93
102
|
return { type: "build-route-res", id: msg.id, ok: false, error: errMsg };
|
|
94
103
|
}
|
|
95
104
|
}
|
|
105
|
+
#sendBuildStatus(
|
|
106
|
+
phase: BuildPhase,
|
|
107
|
+
{ generation, ok, files, message }: { generation?: number; ok: boolean; files?: string[]; message?: string },
|
|
108
|
+
): void {
|
|
109
|
+
if (typeof generation !== "number") return;
|
|
110
|
+
process.send?.({
|
|
111
|
+
type: "build-status",
|
|
112
|
+
data: {
|
|
113
|
+
generation,
|
|
114
|
+
phase,
|
|
115
|
+
ok,
|
|
116
|
+
files: files ?? [],
|
|
117
|
+
message,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
}
|
|
96
121
|
async #enqueueWork<T>(label: string, fn: () => Promise<T>): Promise<T> {
|
|
97
122
|
const started = Date.now();
|
|
98
123
|
const run = this.#workQueue.then(fn, fn);
|
|
@@ -191,10 +216,18 @@ class IncrementalBuilder {
|
|
|
191
216
|
generation: next.generation,
|
|
192
217
|
changedFiles: next.changedFiles,
|
|
193
218
|
});
|
|
219
|
+
this.#sendBuildStatus("css", { generation: next.generation, ok: true, files: next.changedFiles });
|
|
194
220
|
this.#logger.verbose(`css-rebuild checked (${Date.now() - started}ms)`);
|
|
195
221
|
})
|
|
196
222
|
.catch((err) => {
|
|
197
|
-
|
|
223
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
224
|
+
this.#logger.error(`css-rebuild failed: ${message}`);
|
|
225
|
+
this.#sendBuildStatus("css", {
|
|
226
|
+
generation: next.generation,
|
|
227
|
+
ok: false,
|
|
228
|
+
files: next.changedFiles,
|
|
229
|
+
message,
|
|
230
|
+
});
|
|
198
231
|
});
|
|
199
232
|
}, 150);
|
|
200
233
|
}
|
|
@@ -233,25 +266,43 @@ class IncrementalBuilder {
|
|
|
233
266
|
}
|
|
234
267
|
|
|
235
268
|
async #handleWatchBatch(appDir: string, artifactDir: string, batch: ChangeBatch) {
|
|
236
|
-
const
|
|
237
|
-
if (
|
|
269
|
+
const rawKinds = new Set(batch.kinds);
|
|
270
|
+
if (rawKinds.size === 0) return;
|
|
238
271
|
const generation = ++this.#generation;
|
|
239
|
-
|
|
272
|
+
const indexSync = await this.#generatedIndexSync.syncForBatch(batch.files);
|
|
273
|
+
const { files, kinds, expandedBatch, event, hasSyncErrors } = prepareDevWatchBatch({
|
|
274
|
+
generation,
|
|
275
|
+
batch,
|
|
276
|
+
indexSync,
|
|
277
|
+
changePlanner: this.#changePlanner,
|
|
278
|
+
});
|
|
279
|
+
const devPlan = event.devPlan;
|
|
280
|
+
this.#logger.verbose(
|
|
281
|
+
`[hmr] batch generation=${generation} kinds=${kinds.join(",")} files=${files.length} generated=${indexSync.changedFiles.length} roles=${devPlan.roles.join(",") || "(none)"} actions=${devPlan.actions.join(",") || "(none)"}`,
|
|
282
|
+
);
|
|
283
|
+
for (const error of indexSync.errors) this.#logger.error(error);
|
|
240
284
|
|
|
241
285
|
if (kinds.includes("code")) {
|
|
242
286
|
const started = Date.now();
|
|
243
287
|
if (kinds.includes("config")) this.#discovery = await GraphClientEntryDiscovery.create(this.#app);
|
|
244
|
-
else this.#discovery.invalidate?.(
|
|
288
|
+
else this.#discovery.invalidate?.(files);
|
|
245
289
|
this.#logger.verbose(
|
|
246
290
|
`client-entry-discovery ${kinds.includes("config") ? "refreshed" : "invalidated"} (${Date.now() - started}ms)`,
|
|
247
291
|
);
|
|
248
292
|
}
|
|
249
293
|
|
|
250
|
-
if (
|
|
294
|
+
if (hasSyncErrors) {
|
|
295
|
+
this.#sendBuildStatus("barrel", { generation, ok: false, files, message: indexSync.errors.join("\n") });
|
|
296
|
+
process.send?.(event);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (indexSync.changedFiles.length > 0) this.#sendBuildStatus("barrel", { generation, ok: true, files });
|
|
300
|
+
|
|
301
|
+
if (kinds.includes("code") && (await this.batchMayChangePageKeys(appDir, expandedBatch))) {
|
|
251
302
|
const started = Date.now();
|
|
252
303
|
await this.#app.getPageKeys({ refresh: true });
|
|
253
304
|
this.#logger.verbose(`pageKeys updated, app pageKeys are refreshed (${Date.now() - started}ms)`);
|
|
254
|
-
} else if (kinds.includes("code") && this.batchTouchesPagesTree(appDir,
|
|
305
|
+
} else if (kinds.includes("code") && this.batchTouchesPagesTree(appDir, expandedBatch)) {
|
|
255
306
|
this.#logger.verbose("pageKeys refresh skipped; changed page source cannot add/remove a route key");
|
|
256
307
|
}
|
|
257
308
|
|
|
@@ -259,15 +310,17 @@ class IncrementalBuilder {
|
|
|
259
310
|
try {
|
|
260
311
|
const started = Date.now();
|
|
261
312
|
await new CsrArtifactBuilder(this.#app).build();
|
|
313
|
+
this.#sendBuildStatus("csr", { generation, ok: true, files });
|
|
262
314
|
this.#logger.verbose(`csr-rebundle ok (${Date.now() - started}ms)`);
|
|
263
315
|
} catch (err) {
|
|
264
|
-
|
|
316
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
317
|
+
this.#logger.error(`csr-rebundle failed: ${message}`);
|
|
318
|
+
this.#sendBuildStatus("csr", { generation, ok: false, files, message });
|
|
265
319
|
}
|
|
266
320
|
} else if (kinds.includes("code")) {
|
|
267
321
|
this.#logger.verbose(`csr-rebundle skipped; set AKAN_DEV_CSR_REBUILD=1 to enable per-save CSR rebuilds`);
|
|
268
322
|
}
|
|
269
323
|
|
|
270
|
-
const event: BuilderEvent = { type: "invalidate", kinds, files: batch.files, generation };
|
|
271
324
|
process.send?.(event);
|
|
272
325
|
|
|
273
326
|
if (kinds.includes("code")) {
|
|
@@ -276,15 +329,18 @@ class IncrementalBuilder {
|
|
|
276
329
|
const next = await new PagesBundleBuilder(this.#app).build();
|
|
277
330
|
process.send?.({
|
|
278
331
|
type: "pages-updated",
|
|
279
|
-
data: { bundlePath: next.bundlePath, buildId: next.buildId, generation, changedFiles:
|
|
332
|
+
data: { bundlePath: next.bundlePath, buildId: next.buildId, generation, changedFiles: files },
|
|
280
333
|
});
|
|
334
|
+
this.#sendBuildStatus("pages", { generation, ok: true, files });
|
|
281
335
|
this.#logger.verbose(`pages-rebundle ok buildId=${next.buildId} (${Date.now() - started}ms)`);
|
|
282
336
|
} catch (err) {
|
|
283
|
-
|
|
337
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
338
|
+
this.#logger.error(`pages-rebundle failed: ${message}`);
|
|
339
|
+
this.#sendBuildStatus("pages", { generation, ok: false, files, message });
|
|
284
340
|
}
|
|
285
341
|
}
|
|
286
342
|
if (kinds.includes("code") || kinds.includes("css")) {
|
|
287
|
-
this.scheduleCssRebuild(artifactDir, { refresh: true, generation, changedFiles:
|
|
343
|
+
this.scheduleCssRebuild(artifactDir, { refresh: true, generation, changedFiles: files });
|
|
288
344
|
this.#logger.verbose(`css-rebuild scheduled generation=${generation}`);
|
|
289
345
|
}
|
|
290
346
|
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { DevGeneratedIndexSync } from "../frontendBuild";
|
|
3
|
+
import { DevStabilityHarness } from "./devStabilityHarness";
|
|
4
|
+
|
|
5
|
+
const integrationEnabled = process.env.AKAN_DEV_STABILITY_INTEGRATION === "1";
|
|
6
|
+
const INTEGRATION_TIMEOUT_MS = 120_000;
|
|
7
|
+
const harnesses: DevStabilityHarness[] = [];
|
|
8
|
+
|
|
9
|
+
const integrationTest = (name: string, fn: () => Promise<void>): void => {
|
|
10
|
+
if (integrationEnabled) test(name, fn, INTEGRATION_TIMEOUT_MS);
|
|
11
|
+
else test.skip(name, fn);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const createHarness = async (): Promise<DevStabilityHarness> => {
|
|
15
|
+
const harness = new DevStabilityHarness();
|
|
16
|
+
harnesses.push(harness);
|
|
17
|
+
await harness.createFixture();
|
|
18
|
+
return harness;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const isRefreshMessage = (msg: unknown): boolean =>
|
|
22
|
+
typeof msg === "object" &&
|
|
23
|
+
msg !== null &&
|
|
24
|
+
"type" in msg &&
|
|
25
|
+
(msg.type === "client-refresh" || msg.type === "rsc-refresh" || msg.type === "reload");
|
|
26
|
+
|
|
27
|
+
const isBuildStatus =
|
|
28
|
+
(status: "error" | "ok") =>
|
|
29
|
+
(msg: unknown): boolean =>
|
|
30
|
+
typeof msg === "object" &&
|
|
31
|
+
msg !== null &&
|
|
32
|
+
"type" in msg &&
|
|
33
|
+
msg.type === "build-status" &&
|
|
34
|
+
"status" in msg &&
|
|
35
|
+
msg.status === status;
|
|
36
|
+
|
|
37
|
+
const waitForFileIncludes = async (filePath: string, text: string, timeoutMs = 5_000): Promise<string | null> => {
|
|
38
|
+
const started = Date.now();
|
|
39
|
+
while (Date.now() - started < timeoutMs) {
|
|
40
|
+
const file = Bun.file(filePath);
|
|
41
|
+
const contents = (await file.exists()) ? await file.text() : "";
|
|
42
|
+
if (contents.includes(text)) return contents;
|
|
43
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
afterEach(async () => {
|
|
49
|
+
await Promise.all(harnesses.splice(0).map((harness) => harness.cleanup()));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("dev stability integration harness", () => {
|
|
53
|
+
integrationTest("server-only valid edits restart backend without client refresh", async () => {
|
|
54
|
+
const harness = await createHarness();
|
|
55
|
+
const host = await harness.startHost();
|
|
56
|
+
const hmr = await harness.tryConnectHmrProbe();
|
|
57
|
+
const mark = host.markLog();
|
|
58
|
+
const hmrMark = hmr?.mark() ?? 0;
|
|
59
|
+
|
|
60
|
+
await harness.replaceText("srvkit/backendMarker.ts", "initial-backend-marker", "updated-backend-marker");
|
|
61
|
+
|
|
62
|
+
await host.waitForLogSince(mark, /\[backend-reload\]|Shutting down gracefully|stopping backend/);
|
|
63
|
+
await host.waitForLogSince(mark, /backend ready pid=(\d+)|AkanApp gateway is running on port/);
|
|
64
|
+
expect(host.proc.killed).toBe(false);
|
|
65
|
+
expect(host.logs.join("").slice(mark)).not.toMatch(/\[hmr\].*(client-refresh|rsc-refresh)/);
|
|
66
|
+
await hmr?.waitForNoMessageSince(hmrMark, isRefreshMessage);
|
|
67
|
+
hmr?.close();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
integrationTest("client-only valid edits refresh browser state without backend restart", async () => {
|
|
71
|
+
const harness = await createHarness();
|
|
72
|
+
const host = await harness.startHost();
|
|
73
|
+
const hmr = await harness.tryConnectHmrProbe();
|
|
74
|
+
const initialHtml = await harness.tryWaitForHttpText("initial-client-marker", 3_000);
|
|
75
|
+
if (!initialHtml) {
|
|
76
|
+
expect(host.proc.killed).toBe(false);
|
|
77
|
+
hmr?.close();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const mark = host.markLog();
|
|
81
|
+
const hmrMark = hmr?.mark() ?? 0;
|
|
82
|
+
|
|
83
|
+
await harness.replaceText("ui/ClientMarker.tsx", "initial-client-marker", "updated-client-marker");
|
|
84
|
+
|
|
85
|
+
await host.waitForLogSince(mark, /\[dev-plan\].*roles=.*client.*actions=.*rebuild-client/);
|
|
86
|
+
if (hmr) {
|
|
87
|
+
const message = await hmr.waitForMessageSince(hmrMark, isRefreshMessage);
|
|
88
|
+
expect(message).toBeTruthy();
|
|
89
|
+
} else {
|
|
90
|
+
await host.waitForLogSince(mark, /\[hmr\].*(client-refresh|rsc-refresh|reload)|\[SSR\] pages-updated/);
|
|
91
|
+
}
|
|
92
|
+
expect(host.logs.join("").slice(mark)).not.toMatch(/\[backend-reload\]/);
|
|
93
|
+
hmr?.close();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
integrationTest("shared valid edits rebuild client and restart backend in one generation", async () => {
|
|
97
|
+
const harness = await createHarness();
|
|
98
|
+
const host = await harness.startHost();
|
|
99
|
+
const hmr = await harness.tryConnectHmrProbe();
|
|
100
|
+
const initialHtml = await harness.tryWaitForHttpText("initial-shared-marker", 3_000);
|
|
101
|
+
if (!initialHtml) {
|
|
102
|
+
expect(host.proc.killed).toBe(false);
|
|
103
|
+
hmr?.close();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const mark = host.markLog();
|
|
107
|
+
const hmrMark = hmr?.mark() ?? 0;
|
|
108
|
+
|
|
109
|
+
await harness.replaceText("common/marker.ts", "initial-shared-marker", "updated-shared-marker");
|
|
110
|
+
|
|
111
|
+
const plan = await host.waitForLogSince(
|
|
112
|
+
mark,
|
|
113
|
+
/\[dev-plan\] generation=(\d+).*roles=.*shared.*actions=.*rebuild-client.*restart-backend/,
|
|
114
|
+
);
|
|
115
|
+
const generation = plan[1];
|
|
116
|
+
await host.waitForLogSince(mark, new RegExp(`\\[backend-reload\\].*generation=${generation}`));
|
|
117
|
+
if (hmr)
|
|
118
|
+
await hmr.waitForMessageSince(
|
|
119
|
+
hmrMark,
|
|
120
|
+
(msg) =>
|
|
121
|
+
typeof msg === "object" && msg !== null && "generation" in msg && String(msg.generation) === generation,
|
|
122
|
+
);
|
|
123
|
+
else await host.waitForLogSince(mark, new RegExp(`\\[SSR\\] pages-updated.*generation=${generation}`));
|
|
124
|
+
await harness.waitForHttpText("updated-shared-marker");
|
|
125
|
+
hmr?.close();
|
|
126
|
+
});
|
|
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
|
+
|
|
162
|
+
integrationTest("client build failure reports error and recovers after fix", async () => {
|
|
163
|
+
const harness = await createHarness();
|
|
164
|
+
const host = await harness.startHost();
|
|
165
|
+
const hmr = await harness.tryConnectHmrProbe();
|
|
166
|
+
const initialHtml = await harness.tryWaitForHttpText("initial-client-marker", 3_000);
|
|
167
|
+
if (!initialHtml) {
|
|
168
|
+
expect(host.proc.killed).toBe(false);
|
|
169
|
+
hmr?.close();
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const failureMark = host.markLog();
|
|
173
|
+
const failureHmrMark = hmr?.mark() ?? 0;
|
|
174
|
+
|
|
175
|
+
await harness.writeFile(
|
|
176
|
+
"ui/ClientMarker.tsx",
|
|
177
|
+
`export function ClientMarker() {
|
|
178
|
+
return <p>broken</p>
|
|
179
|
+
`,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
await host.waitForLogSince(
|
|
183
|
+
failureMark,
|
|
184
|
+
/\[build-status\].*phase=pages.*ok=false|\[build-status\].*phase=csr.*ok=false/,
|
|
185
|
+
);
|
|
186
|
+
if (hmr) await hmr.waitForMessageSince(failureHmrMark, isBuildStatus("error"));
|
|
187
|
+
await harness.waitForHttpText("initial-client-marker");
|
|
188
|
+
const recoveryMark = host.markLog();
|
|
189
|
+
const recoveryHmrMark = hmr?.mark() ?? 0;
|
|
190
|
+
|
|
191
|
+
await harness.writeFile(
|
|
192
|
+
"ui/ClientMarker.tsx",
|
|
193
|
+
`export function ClientMarker() {
|
|
194
|
+
return <p data-testid="client-marker">recovered-client-marker</p>;
|
|
195
|
+
}
|
|
196
|
+
`,
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
await host.waitForLogSince(recoveryMark, /\[build-status\].*ok=true/);
|
|
200
|
+
if (hmr) await hmr.waitForMessageSince(recoveryHmrMark, isBuildStatus("ok"));
|
|
201
|
+
await harness.waitForHttpText("recovered-client-marker");
|
|
202
|
+
hmr?.close();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
integrationTest("barrel add/delete includes generated indexes in watch generation", async () => {
|
|
206
|
+
const harness = await createHarness();
|
|
207
|
+
const sync = new DevGeneratedIndexSync({ workspaceRoot: harness.workspaceRoot });
|
|
208
|
+
const facets = ["common", "ui"] as const;
|
|
209
|
+
|
|
210
|
+
for (const facet of facets) {
|
|
211
|
+
const exportName = `${facet}TmpExample`;
|
|
212
|
+
const indexPath = `${facet}/index.ts`;
|
|
213
|
+
const absChangedFile = `${harness.appDir}/${facet}/tmpExample.ts`;
|
|
214
|
+
const absIndexPath = `${harness.appDir}/${indexPath}`;
|
|
215
|
+
|
|
216
|
+
await harness.writeFile(
|
|
217
|
+
`${facet}/tmpExample.ts`,
|
|
218
|
+
`export const ${exportName} = "added-${facet}-example";
|
|
219
|
+
`,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const added = await sync.syncForBatch([absChangedFile]);
|
|
223
|
+
expect(added.errors).toEqual([]);
|
|
224
|
+
expect(added.changedFiles).toContain(absIndexPath);
|
|
225
|
+
const addedIndex = await waitForFileIncludes(absIndexPath, "tmpExample");
|
|
226
|
+
expect(addedIndex).not.toBeNull();
|
|
227
|
+
expect(addedIndex ?? "").toContain("tmpExample");
|
|
228
|
+
|
|
229
|
+
await harness.removeFile(`${facet}/tmpExample.ts`);
|
|
230
|
+
const removed = await sync.syncForBatch([absChangedFile]);
|
|
231
|
+
expect(removed.errors).toEqual([]);
|
|
232
|
+
expect(removed.changedFiles).toContain(absIndexPath);
|
|
233
|
+
const deletedIndex = await waitForFileIncludes(absIndexPath, "tmpExample", 1_000);
|
|
234
|
+
if (deletedIndex) throw new Error(`${indexPath} still contains tmpExample after delete`);
|
|
235
|
+
const finalIndex = await Bun.file(absIndexPath).text();
|
|
236
|
+
expect(finalIndex).toBeString();
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
integrationTest("route and css phase-5 scope remains smoke-level in this harness", async () => {
|
|
241
|
+
const manualSmoke = [
|
|
242
|
+
"route add/delete should be covered by a later browser-driven test",
|
|
243
|
+
"css build failure should preserve active stylesheet and report build-status",
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
expect(manualSmoke).toHaveLength(2);
|
|
247
|
+
});
|
|
248
|
+
});
|