@akanjs/devkit 3.0.0-beta.24 → 3.0.0-beta.25
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 +37 -22
- package/akanApp/akanApp.host.ts +34 -1
- package/akanApp/devBootLatch.test.ts +39 -0
- package/akanApp/devBootLatch.ts +39 -0
- package/akanApp/devHostPolicy.test.ts +25 -0
- package/akanApp/devHostPolicy.ts +27 -2
- package/cloud/globalConfig.ts +3 -17
- package/csrE2e/csrE2eHarness.fixture.ts +66 -11
- package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
- package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
- package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
- package/fileSys.ts +16 -1
- package/frontendBuild/buildRouteClient.test.ts +61 -0
- package/frontendBuild/bundleDefine.test.ts +12 -0
- package/frontendBuild/bundleDefine.ts +5 -1
- package/frontendBuild/clientEntryDiscovery.test.ts +37 -0
- package/frontendBuild/clientEntryDiscovery.ts +10 -5
- package/frontendBuild/csrArtifactBuilder.ts +3 -6
- package/frontendBuild/csrDevArtifactWriter.test.ts +143 -0
- package/frontendBuild/csrDevArtifactWriter.ts +158 -24
- package/frontendBuild/csrDevBundler.ts +44 -205
- package/frontendBuild/csrDevModuleCompiler.ts +51 -9
- package/frontendBuild/csrDevPatcher.test.ts +340 -0
- package/frontendBuild/csrDevPatcher.ts +374 -0
- package/frontendBuild/csrDevPaths.ts +60 -0
- package/frontendBuild/csrDevResolver.test.ts +165 -0
- package/frontendBuild/csrDevResolver.ts +70 -18
- package/frontendBuild/csrDevTypes.ts +14 -1
- package/frontendBuild/csrEntryFiles.ts +24 -0
- package/frontendBuild/cssCompiler.ts +2 -4
- package/frontendBuild/devRegistryBundler.ts +177 -0
- package/frontendBuild/hmrWatcher.test.ts +51 -0
- package/frontendBuild/hmrWatcher.ts +17 -3
- package/frontendBuild/index.ts +3 -0
- package/frontendBuild/pagesBundleBuilder.ts +24 -1
- package/frontendBuild/pagesEntrySourceGenerator.ts +9 -2
- package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
- package/frontendBuild/registryMetadataFingerprint.ts +28 -0
- package/frontendBuild/routeClientBuilder.ts +79 -18
- package/frontendBuild/serverGraphFile.test.ts +77 -0
- package/frontendBuild/serverGraphFile.ts +93 -0
- package/frontendBuild/ssrDevBundler.ts +91 -0
- package/incrementalBuilder/buildBatch.proc.ts +94 -16
- package/incrementalBuilder/buildBatchProtocol.ts +6 -3
- package/incrementalBuilder/buildBatchRunner.test.ts +31 -0
- package/incrementalBuilder/buildBatchRunner.ts +17 -8
- package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
- package/incrementalBuilder/builderWorkQueue.ts +126 -0
- package/incrementalBuilder/incrementalBuilder.host.test.ts +1 -1
- package/incrementalBuilder/incrementalBuilder.host.ts +32 -3
- package/incrementalBuilder/incrementalBuilder.proc.test.ts +129 -2
- package/incrementalBuilder/incrementalBuilder.proc.ts +518 -52
- package/integration/devStability.integration.test.ts +18 -7
- package/package.json +2 -2
- package/transforms/rscUseClientTransform.ts +7 -0
- package/transforms/transforms.test.ts +13 -0
- package/transforms/useClientBundlePlugin.ts +4 -1
|
@@ -24,7 +24,7 @@ const isRefreshMessage = (msg: unknown): boolean =>
|
|
|
24
24
|
typeof msg === "object" &&
|
|
25
25
|
msg !== null &&
|
|
26
26
|
"type" in msg &&
|
|
27
|
-
(msg.type === "
|
|
27
|
+
(msg.type === "ssr-update" || msg.type === "rsc-refresh" || msg.type === "reload");
|
|
28
28
|
|
|
29
29
|
const isBuildStatus =
|
|
30
30
|
(status: "error" | "ok") =>
|
|
@@ -148,7 +148,7 @@ describe("dev stability integration harness", () => {
|
|
|
148
148
|
await host.waitForLogSince(mark, /\[backend-reload\]|Shutting down gracefully|stopping backend/);
|
|
149
149
|
await host.waitForLogSince(mark, /backend ready pid=(\d+)|AkanApp gateway is running on port/);
|
|
150
150
|
expect(host.proc.killed).toBe(false);
|
|
151
|
-
expect(host.logs.join("").slice(mark)).not.toMatch(/\[hmr\].*
|
|
151
|
+
expect(host.logs.join("").slice(mark)).not.toMatch(/\[hmr\].*rsc-refresh|\[ssr\] registry generation/);
|
|
152
152
|
await hmr?.waitForNoMessageSince(hmrMark, isRefreshMessage);
|
|
153
153
|
hmr?.close();
|
|
154
154
|
});
|
|
@@ -179,7 +179,10 @@ describe("dev stability integration harness", () => {
|
|
|
179
179
|
if (hmr) {
|
|
180
180
|
await expectHmrMessage(hmr, hmrMark, isRefreshMessage, "a client refresh");
|
|
181
181
|
} else {
|
|
182
|
-
await host.waitForLogSince(
|
|
182
|
+
await host.waitForLogSince(
|
|
183
|
+
mark,
|
|
184
|
+
/\[hmr\].*(rsc-refresh|reload)|\[ssr\] registry generation|\[SSR\] pages-updated/,
|
|
185
|
+
);
|
|
183
186
|
}
|
|
184
187
|
expect(host.logs.join("").slice(mark)).not.toMatch(/\[backend-reload\]/);
|
|
185
188
|
hmr?.close();
|
|
@@ -487,7 +490,8 @@ describe("dev resource budgets", () => {
|
|
|
487
490
|
`csr-armed-marker-${attempt}`,
|
|
488
491
|
),
|
|
489
492
|
);
|
|
490
|
-
|
|
493
|
+
// A worker logs `csr-dev generation=N patch`; the resident builder's patcher logs `csr-patch generation=N patch`.
|
|
494
|
+
await host.waitForLogSince(resyncMark, /csr-rebundle ok|csr-(?:dev|patch) generation=\d+ patch/, WAIT_MS);
|
|
491
495
|
});
|
|
492
496
|
|
|
493
497
|
budgetTest("bounds the rsc worker and the tree across repeated saves", async () => {
|
|
@@ -498,6 +502,9 @@ describe("dev resource budgets", () => {
|
|
|
498
502
|
env: { AKAN_RSC_WORKER_MAX_RELOADS: "1", AKAN_RSC_WORKER_MIN_RECYCLE_INTERVAL_MS: "1" },
|
|
499
503
|
});
|
|
500
504
|
await harness.waitForHttpText("initial-client-marker", WAIT_MS);
|
|
505
|
+
// The first page no longer waits for the SSR registry's boot build; the host logs when its worker is done, whether or
|
|
506
|
+
// not a backend was up to take the build's announcement.
|
|
507
|
+
await host.waitForLogSince(0, /\[builder\] boot builds settled/, WAIT_MS);
|
|
501
508
|
|
|
502
509
|
const idleTotal = await DevStabilityHarness.processTreeRssBytes(host.proc.pid);
|
|
503
510
|
const idleWithoutBuilder = await DevStabilityHarness.processTreeRssBytes(host.proc.pid, { excludeBuilder: true });
|
|
@@ -543,7 +550,7 @@ describe("dev resource budgets", () => {
|
|
|
543
550
|
|
|
544
551
|
budgetTest("recycles the builder at an unmeetable ceiling and keeps developing through it", async () => {
|
|
545
552
|
const harness = await createHarness();
|
|
546
|
-
//
|
|
553
|
+
// Below the builder once it has served the first route build (about 380MiB), which arms the recycle.
|
|
547
554
|
const host = await harness.startHost({ timeoutMs: BOOT_MS, env: { AKAN_BUILDER_MAX_RSS_MB: "200" } });
|
|
548
555
|
const start = host.markLog();
|
|
549
556
|
await harness.waitForHttpText("initial-client-marker", WAIT_MS);
|
|
@@ -579,14 +586,18 @@ describe("dev resource budgets", () => {
|
|
|
579
586
|
);
|
|
580
587
|
await harness.waitForHttpText("marker-after-recycle", WAIT_MS);
|
|
581
588
|
|
|
582
|
-
// A
|
|
589
|
+
// A ceiling the replacement is back over within the interval is reported, and still enforced (recycles are
|
|
590
|
+
// throttled), never disabled for the session. Whether it is back over depends on the machine's allocator, so the
|
|
591
|
+
// warning is asserted only when it is; the decision itself is unit-tested (devHostPolicy).
|
|
583
592
|
for (let i = 1; i <= 3; i++) {
|
|
584
593
|
const { mark } = await harness.editUntilSeen(host, (attempt) =>
|
|
585
594
|
harness.replaceText("ui/ClientMarker.tsx", /marker(-[\w-]+)?/, `marker-settled-${i}-${attempt}`),
|
|
586
595
|
);
|
|
587
596
|
await host.waitForLogSince(mark, /pages-rebundle ok/, WAIT_MS).catch(() => undefined);
|
|
588
597
|
}
|
|
589
|
-
await host.
|
|
598
|
+
const settled = await DevStabilityHarness.builderProcess(host.proc.pid);
|
|
599
|
+
if ((settled?.rssBytes ?? 0) >= 200 * MB)
|
|
600
|
+
await host.waitForLogSince(start, /ceiling costs about one boot build per interval/, WAIT_MS);
|
|
590
601
|
expect(host.logs.join("").slice(start)).not.toMatch(/no longer enforcing it this session/);
|
|
591
602
|
});
|
|
592
603
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "3.0.0-beta.
|
|
3
|
+
"version": "3.0.0-beta.25",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"@earendil-works/pi-tui": "0.86.1",
|
|
48
48
|
"@inquirer/prompts": "^8.4.3",
|
|
49
49
|
"@tailwindcss/node": "^4.3.0",
|
|
50
|
-
"akanjs": "3.0.0-beta.
|
|
50
|
+
"akanjs": "3.0.0-beta.25",
|
|
51
51
|
"chalk": "^5.6.2",
|
|
52
52
|
"commander": "^14.0.3",
|
|
53
53
|
"dayjs": "^1.11.20",
|
|
@@ -10,8 +10,12 @@ const STAR_REEXPORT_HINT_RE = /\bexport\s*\*\s*from\b/;
|
|
|
10
10
|
export interface UseClientTransformArgs {
|
|
11
11
|
path: string;
|
|
12
12
|
workspaceRoot?: string;
|
|
13
|
+
/** Told each `"use client"` module's export names, the only part of it the server graph holds. */
|
|
14
|
+
onClientModule?: (path: string, exports: string[]) => void;
|
|
13
15
|
}
|
|
14
16
|
|
|
17
|
+
export const hasUseClientDirective = (source: string): boolean => USE_CLIENT_RE.test(source);
|
|
18
|
+
|
|
15
19
|
export function toClientReferencePath(absPath: string, workspaceRoot: string): string {
|
|
16
20
|
return path.relative(path.resolve(workspaceRoot), path.resolve(absPath)).split(path.sep).join("/");
|
|
17
21
|
}
|
|
@@ -51,7 +55,10 @@ export function transformUseClient(source: string, args: UseClientTransformArgs)
|
|
|
51
55
|
if (!USE_CLIENT_RE.test(source)) return null;
|
|
52
56
|
if (IMPLICIT_ROOT_LAYOUT_RE.test(args.path)) return null;
|
|
53
57
|
const exports = scanUseClientExports(source, args.path, args.workspaceRoot);
|
|
58
|
+
//? Not reported without exports: the module then stays in the server bundle as it is, so a save of it changes what
|
|
59
|
+
//? the server runs, which a client module's edit that kept its export names does not.
|
|
54
60
|
if (exports.length === 0) return null;
|
|
61
|
+
args.onClientModule?.(args.path, exports);
|
|
55
62
|
|
|
56
63
|
const referencePath = args.workspaceRoot ? toClientReferencePath(args.path, args.workspaceRoot) : args.path;
|
|
57
64
|
const filePathLit = JSON.stringify(referencePath);
|
|
@@ -27,6 +27,19 @@ describe("transformUseClient", () => {
|
|
|
27
27
|
).toBeNull();
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
+
test("a client module with no exports stays server code and is not reported as a client module", () => {
|
|
31
|
+
const reported: string[] = [];
|
|
32
|
+
const onClientModule = (file: string) => reported.push(file);
|
|
33
|
+
expect(
|
|
34
|
+
transformUseClient('"use client";\nimport "./side-effect";\n', { path: "/repo/ui/Fx.tsx", onClientModule }),
|
|
35
|
+
).toBeNull();
|
|
36
|
+
expect(reported).toEqual([]);
|
|
37
|
+
expect(
|
|
38
|
+
transformUseClient('"use client"; export const value = 1;', { path: "/repo/ui/V.tsx", onClientModule }),
|
|
39
|
+
).not.toBeNull();
|
|
40
|
+
expect(reported).toEqual(["/repo/ui/V.tsx"]);
|
|
41
|
+
});
|
|
42
|
+
|
|
30
43
|
test("stubs named and default exports as RSC client references", () => {
|
|
31
44
|
const source = [
|
|
32
45
|
"// comment before directive",
|
|
@@ -3,7 +3,9 @@ import { loaderFor } from "./moduleSyntax";
|
|
|
3
3
|
import { transformUseClient } from "./rscUseClientTransform";
|
|
4
4
|
|
|
5
5
|
/** `workspaceRoot` makes reference keys workspace-relative; they must match the client manifest's keys. */
|
|
6
|
-
export function createUseClientBundlePlugin(
|
|
6
|
+
export function createUseClientBundlePlugin(
|
|
7
|
+
options: { workspaceRoot?: string; onClientModule?: (path: string, exports: string[]) => void } = {},
|
|
8
|
+
): BunPlugin {
|
|
7
9
|
return {
|
|
8
10
|
name: "akan-use-client-bundle",
|
|
9
11
|
setup(build) {
|
|
@@ -18,6 +20,7 @@ export function createUseClientBundlePlugin(options: { workspaceRoot?: string }
|
|
|
18
20
|
const stubbed = transformUseClient(source, {
|
|
19
21
|
path: args.path,
|
|
20
22
|
workspaceRoot: options.workspaceRoot,
|
|
23
|
+
onClientModule: options.onClientModule,
|
|
21
24
|
});
|
|
22
25
|
if (stubbed === null) return undefined;
|
|
23
26
|
return { contents: stubbed, loader: loaderFor(args.path) };
|