@akanjs/devkit 3.0.0-beta.23 → 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.
Files changed (60) hide show
  1. package/DEV_RUNTIME_KNOBS.md +37 -22
  2. package/akanApp/akanApp.host.ts +34 -1
  3. package/akanApp/devBootLatch.test.ts +39 -0
  4. package/akanApp/devBootLatch.ts +39 -0
  5. package/akanApp/devHostPolicy.test.ts +25 -0
  6. package/akanApp/devHostPolicy.ts +27 -2
  7. package/cloud/globalConfig.ts +3 -2
  8. package/csrE2e/csrE2eHarness.fixture.ts +455 -0
  9. package/csrE2e/csrFastRefresh.e2e.test.ts +233 -0
  10. package/csrE2e/csrPageStack.e2e.test.ts +187 -0
  11. package/csrE2e/csrPatchFirst.e2e.test.ts +87 -0
  12. package/csrE2e/devHmrLatency.e2e.test.ts +391 -0
  13. package/csrE2e/ssrRegistry.e2e.test.ts +644 -0
  14. package/fileSys.ts +16 -1
  15. package/frontendBuild/buildRouteClient.test.ts +61 -0
  16. package/frontendBuild/bundleDefine.test.ts +12 -0
  17. package/frontendBuild/bundleDefine.ts +5 -1
  18. package/frontendBuild/clientEntryDiscovery.test.ts +37 -0
  19. package/frontendBuild/clientEntryDiscovery.ts +10 -5
  20. package/frontendBuild/csrArtifactBuilder.ts +3 -6
  21. package/frontendBuild/csrDevArtifactWriter.test.ts +143 -0
  22. package/frontendBuild/csrDevArtifactWriter.ts +303 -0
  23. package/frontendBuild/csrDevBundler.test.ts +66 -0
  24. package/frontendBuild/csrDevBundler.ts +95 -0
  25. package/frontendBuild/csrDevModuleCompiler.ts +304 -0
  26. package/frontendBuild/csrDevPatcher.test.ts +340 -0
  27. package/frontendBuild/csrDevPatcher.ts +374 -0
  28. package/frontendBuild/csrDevPaths.ts +124 -0
  29. package/frontendBuild/csrDevResolver.test.ts +165 -0
  30. package/frontendBuild/csrDevResolver.ts +234 -0
  31. package/frontendBuild/csrDevTypes.ts +59 -0
  32. package/frontendBuild/csrEntryFiles.ts +24 -0
  33. package/frontendBuild/cssCompiler.ts +2 -4
  34. package/frontendBuild/devRegistryBundler.ts +177 -0
  35. package/frontendBuild/hmrWatcher.test.ts +51 -0
  36. package/frontendBuild/hmrWatcher.ts +17 -3
  37. package/frontendBuild/index.ts +4 -0
  38. package/frontendBuild/pagesBundleBuilder.ts +24 -1
  39. package/frontendBuild/pagesEntrySourceGenerator.ts +56 -6
  40. package/frontendBuild/registryMetadataFingerprint.test.ts +63 -0
  41. package/frontendBuild/registryMetadataFingerprint.ts +28 -0
  42. package/frontendBuild/routeClientBuilder.ts +79 -18
  43. package/frontendBuild/serverGraphFile.test.ts +77 -0
  44. package/frontendBuild/serverGraphFile.ts +93 -0
  45. package/frontendBuild/ssrDevBundler.ts +91 -0
  46. package/incrementalBuilder/buildBatch.proc.ts +119 -6
  47. package/incrementalBuilder/buildBatchProtocol.ts +6 -3
  48. package/incrementalBuilder/buildBatchRunner.test.ts +31 -0
  49. package/incrementalBuilder/buildBatchRunner.ts +17 -8
  50. package/incrementalBuilder/builderWorkQueue.test.ts +94 -0
  51. package/incrementalBuilder/builderWorkQueue.ts +126 -0
  52. package/incrementalBuilder/incrementalBuilder.host.test.ts +1 -1
  53. package/incrementalBuilder/incrementalBuilder.host.ts +33 -3
  54. package/incrementalBuilder/incrementalBuilder.proc.test.ts +129 -2
  55. package/incrementalBuilder/incrementalBuilder.proc.ts +518 -52
  56. package/integration/devStability.integration.test.ts +18 -7
  57. package/package.json +3 -3
  58. package/transforms/rscUseClientTransform.ts +7 -0
  59. package/transforms/transforms.test.ts +13 -0
  60. 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 === "client-refresh" || msg.type === "rsc-refresh" || msg.type === "reload");
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\].*(client-refresh|rsc-refresh)/);
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(mark, /\[hmr\].*(client-refresh|rsc-refresh|reload)|\[SSR\] pages-updated/);
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
- await host.waitForLogSince(resyncMark, /csr-rebundle ok/, WAIT_MS);
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
- // Deliberately below the post-boot builder: it no longer grows into a ceiling, so it must start over one.
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 tight ceiling is reported, and still enforced (recycles are throttled), never disabled for the session.
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.waitForLogSince(start, /ceiling costs about one boot build per interval/, WAIT_MS);
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.23",
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.23",
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",
@@ -63,7 +63,7 @@
63
63
  "typescript": "^6.0.3"
64
64
  },
65
65
  "peerDependencies": {
66
- "react": "19.2.7"
66
+ "react": "19.3.0"
67
67
  },
68
68
  "peerDependenciesMeta": {
69
69
  "react": {
@@ -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(options: { workspaceRoot?: string } = {}): BunPlugin {
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) };