@akanjs/cli 3.0.0-alpha.4 → 3.0.0-alpha.5

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/.build-stamp +1 -1
  2. package/{abstractCompactor-rk6twzvj.js → abstractCompactor-e5yq60qz.js} +2 -2
  3. package/{agent.command-jfz8yfwy.js → agent.command-b0fcx3r2.js} +7 -7
  4. package/{application.command-mxyqtd6p.js → application.command-cgmy00gd.js} +5 -5
  5. package/{applicationBuildRunner-cwk52sb7.js → applicationBuildRunner-n1svrwgt.js} +4 -4
  6. package/{applicationReleasePackager-yrrhbkv3.js → applicationReleasePackager-gg8ccbeh.js} +1 -1
  7. package/buildBatch.proc.js +4 -4
  8. package/{capacitorApp-j357ak1g.js → capacitorApp-yyw35vg4.js} +3 -5
  9. package/{cloud.command-f87dwe52.js → cloud.command-1m7hfdpk.js} +9 -9
  10. package/{context.command-z5bf8bn7.js → context.command-b5heat2a.js} +15 -15
  11. package/{guideline.command-1m1ctar3.js → guideline.command-9ew6aqhm.js} +5 -5
  12. package/guidelines/ssrRule/ssrRule.generate.json +31 -0
  13. package/guidelines/ssrRule/ssrRule.instruction.md +108 -0
  14. package/incrementalBuilder.proc.js +3 -3
  15. package/{index-3pgtw35d.js → index-0cj2zxbm.js} +1 -1
  16. package/{index-cap2k2ch.js → index-0fj4yg7k.js} +52 -17
  17. package/{index-jtm86byd.js → index-47qxw1tz.js} +28 -22
  18. package/{index-vhv3xnjz.js → index-60a3jfcz.js} +6 -6
  19. package/{index-b978vs2e.js → index-6npsfpxr.js} +3 -3
  20. package/{index-csqk9z5j.js → index-c94nq3c1.js} +3 -3
  21. package/{index-cn24dcqn.js → index-exhpp31y.js} +2 -2
  22. package/{index-hjzr8ja1.js → index-hrwzz2wf.js} +1 -1
  23. package/{index-rh16j3c7.js → index-j9cxndc0.js} +3 -2
  24. package/{index-jk9q6pse.js → index-jkynkyfk.js} +1 -1
  25. package/{index-4yp06pn1.js → index-jn9vfpwz.js} +9 -31
  26. package/{index-4zgs8g1w.js → index-m4kpmmtj.js} +1 -1
  27. package/{index-e3qskyt2.js → index-meh7mmz9.js} +2 -2
  28. package/{index-ybybkec0.js → index-mn96y1nk.js} +6 -6
  29. package/{index-y0k42k04.js → index-mterpj70.js} +17 -86
  30. package/{index-6pz1j0zj.js → index-mxvakhsm.js} +6 -2
  31. package/{index-n0z3f2xg.js → index-nfxq39kj.js} +4 -4
  32. package/{index-tq1y56tp.js → index-nx1vbtsc.js} +35 -12
  33. package/{index-1s71fwrq.js → index-p69fcwe1.js} +11 -11
  34. package/{index-xj8bks3p.js → index-p6txr1ss.js} +4 -4
  35. package/{index-r4w8cry5.js → index-pfrt7qwh.js} +4 -4
  36. package/{index-xwgm94zr.js → index-qp089jp0.js} +1 -1
  37. package/{index-fmgbwsr5.js → index-wrrb8rxk.js} +4 -4
  38. package/index.js +19 -19
  39. package/{library.command-pwfx8dx2.js → library.command-aa1g8nsa.js} +4 -4
  40. package/{localRegistry.command-r4zjx5py.js → localRegistry.command-kshva1ck.js} +8 -8
  41. package/{module.command-0t3ewata.js → module.command-b1zzgqde.js} +8 -8
  42. package/{package.command-7hmkxvpa.js → package.command-9qv6vr9s.js} +4 -4
  43. package/package.json +2 -2
  44. package/{page.command-st6r4s0q.js → page.command-k16pw244.js} +4 -4
  45. package/{primitive.command-1qkxpzsn.js → primitive.command-s60dn1ae.js} +9 -9
  46. package/{quality.command-hy5vt1bh.js → quality.command-p3vaze52.js} +456 -71
  47. package/{repair.command-nakkq2zp.js → repair.command-mfv749ha.js} +6 -6
  48. package/{scalar.command-fjq272gs.js → scalar.command-hs3nwzv1.js} +7 -7
  49. package/templates/lib/db.ts +3 -1
  50. package/templates/lib/useClient.ts +9 -3
  51. package/templates/workspaceRoot/.cursor/rules/akan-sync-conventions.mdc.template +3 -2
  52. package/templates/workspaceRoot/.cursor/rules/client-server-boundaries.mdc.template +1 -0
  53. package/templates/workspaceRoot/.cursor/rules/ssr-first.mdc.template +59 -0
  54. package/templates/workspaceRoot/AGENTS.md.template +161 -25
  55. package/templates/workspaceRoot/biome.json.template +22 -19
  56. package/templates/workspaceRoot/docs/AI-DEVELOPMENT.md.template +1 -4
  57. package/{workflow.command-jswbgk90.js → workflow.command-z0e0fk0z.js} +12 -12
  58. package/{workspace.command-h9wwcjpt.js → workspace.command-3q1d6kk4.js} +21 -21
  59. package/templates/libRoot/base/baseLogic.ts +0 -14
  60. package/templates/libRoot/base/index.ts +0 -8
package/.build-stamp CHANGED
@@ -1 +1 @@
1
- 52e4d869ab5170b4db59ce979635a10421003c20193f71051a8df3007f2ecf38
1
+ a0eaadfb9b58c3aa21817d33fb8bc0ad7889c719fb293715682a39a426dc01b9
@@ -1,11 +1,11 @@
1
1
  // @bun
2
2
  import {
3
3
  AiSession
4
- } from "./index-3pgtw35d.js";
4
+ } from "./index-0cj2zxbm.js";
5
5
  import {
6
6
  AbstractDoc
7
7
  } from "./index-0wae5ebk.js";
8
- import"./index-6pz1j0zj.js";
8
+ import"./index-mxvakhsm.js";
9
9
  import"./index-67546d0j.js";
10
10
  import"./index-r24hmh0q.js";
11
11
 
@@ -1,16 +1,16 @@
1
1
  // @bun
2
2
  import {
3
3
  AgentScript
4
- } from "./index-n0z3f2xg.js";
5
- import"./index-4yp06pn1.js";
6
- import"./index-4zgs8g1w.js";
7
- import"./index-xwgm94zr.js";
4
+ } from "./index-nfxq39kj.js";
5
+ import"./index-jn9vfpwz.js";
6
+ import"./index-m4kpmmtj.js";
7
+ import"./index-qp089jp0.js";
8
8
  import {
9
9
  Workspace,
10
10
  command
11
- } from "./index-jk9q6pse.js";
12
- import"./index-jtm86byd.js";
13
- import"./index-6pz1j0zj.js";
11
+ } from "./index-jkynkyfk.js";
12
+ import"./index-47qxw1tz.js";
13
+ import"./index-mxvakhsm.js";
14
14
  import"./index-xys926f2.js";
15
15
  import"./index-67546d0j.js";
16
16
  import"./index-r24hmh0q.js";
@@ -4,22 +4,22 @@ import {
4
4
  } from "./index-0wae5ebk.js";
5
5
  import {
6
6
  ApplicationScript
7
- } from "./index-tq1y56tp.js";
7
+ } from "./index-nx1vbtsc.js";
8
8
  import {
9
9
  getMobileTargetChoices
10
10
  } from "./index-76rn3g2c.js";
11
- import"./index-cn24dcqn.js";
11
+ import"./index-exhpp31y.js";
12
12
  import {
13
13
  App,
14
14
  Exec,
15
15
  Sys,
16
16
  Workspace,
17
17
  command
18
- } from "./index-jk9q6pse.js";
18
+ } from "./index-jkynkyfk.js";
19
19
  import"./index-fgc8r6dj.js";
20
20
  import"./index-bjpxzr6s.js";
21
- import"./index-jtm86byd.js";
22
- import"./index-6pz1j0zj.js";
21
+ import"./index-47qxw1tz.js";
22
+ import"./index-mxvakhsm.js";
23
23
  import"./index-67546d0j.js";
24
24
  import"./index-r24hmh0q.js";
25
25
 
@@ -4,13 +4,13 @@ import {
4
4
  CsrArtifactBuilder,
5
5
  SsrBaseArtifactBuilder,
6
6
  precompressArtifacts
7
- } from "./index-cap2k2ch.js";
7
+ } from "./index-0fj4yg7k.js";
8
8
  import"./index-xjd3rqzj.js";
9
- import"./index-rh16j3c7.js";
9
+ import"./index-j9cxndc0.js";
10
10
  import"./index-bjpxzr6s.js";
11
11
  import {
12
12
  Spinner
13
- } from "./index-6pz1j0zj.js";
13
+ } from "./index-mxvakhsm.js";
14
14
  import"./index-r24hmh0q.js";
15
15
 
16
16
  // pkgs/@akanjs/devkit/applicationBuildRunner.ts
@@ -66,7 +66,7 @@ class ApplicationBuildRunner {
66
66
  await this.#runPhase("backend", "Compiling backend", () => this.#buildBackend(), (result) => `${result.entrypoints} entrypoints, ${result.outputs} outputs`, phaseOptions);
67
67
  await this.#runPhase("ssr", "Building SSR route artifacts", () => this.#buildSsr(), (result) => result ? `${result.allRoutes.manifest.routeIds.length} routes, ${result.allRoutes.manifest.knownEntries.length} entries` : "skipped", phaseOptions);
68
68
  await this.#runPhase("csr", "Building CSR assets", () => this.#buildCsr(), (result) => result?.outputDir ?? "skipped", phaseOptions);
69
- await this.#runPhase("compress", "Compressing static assets", () => precompressArtifacts(this.#app), (result) => result.files > 0 ? `${result.files} files, ${ApplicationBuildRunner.formatBytes(result.inputBytes)} -> ${ApplicationBuildRunner.formatBytes(result.outputBytes)}` : "no files", phaseOptions);
69
+ await this.#runPhase("compress", "Compressing static assets", () => precompressArtifacts(this.#app), (result) => result.files > 0 ? `${result.files} files, ${ApplicationBuildRunner.formatBytes(result.inputBytes)} -> gzip ${ApplicationBuildRunner.formatBytes(result.outputBytes)} / br ${ApplicationBuildRunner.formatBytes(result.brotliBytes)}` : "no files", phaseOptions);
70
70
  await this.#runPhase("metadata", "Writing production metadata", () => this.#buildAppMeta(), undefined, {
71
71
  spinner
72
72
  });
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  Spinner
4
- } from "./index-6pz1j0zj.js";
4
+ } from "./index-mxvakhsm.js";
5
5
  import {
6
6
  FileSys
7
7
  } from "./index-67546d0j.js";
@@ -5,15 +5,15 @@ import {
5
5
  FontOptimizer,
6
6
  PagesBundleBuilder,
7
7
  SsrBaseArtifactBuilder
8
- } from "./index-cap2k2ch.js";
8
+ } from "./index-0fj4yg7k.js";
9
9
  import"./index-xjd3rqzj.js";
10
- import"./index-rh16j3c7.js";
10
+ import"./index-j9cxndc0.js";
11
11
  import"./index-bjpxzr6s.js";
12
12
  import {
13
13
  AppExecutor,
14
14
  WorkspaceExecutor
15
- } from "./index-jtm86byd.js";
16
- import"./index-6pz1j0zj.js";
15
+ } from "./index-47qxw1tz.js";
16
+ import"./index-mxvakhsm.js";
17
17
  import"./index-67546d0j.js";
18
18
  import"./index-r24hmh0q.js";
19
19
 
@@ -11,7 +11,6 @@ import {
11
11
  formatAndroidReleaseSigningError,
12
12
  formatIosRunFailureMessage,
13
13
  getAdbDeviceStateIssues,
14
- getAndroidLocalServerHost,
15
14
  getMissingAndroidReleaseSigningKeys,
16
15
  isPlaceholderAppId,
17
16
  materializeCapacitorConfig,
@@ -25,10 +24,10 @@ import {
25
24
  selectLocalDevHost,
26
25
  sortIosRunTargets,
27
26
  writeRootCapacitorConfig
28
- } from "./index-y0k42k04.js";
27
+ } from "./index-mterpj70.js";
29
28
  import"./index-76rn3g2c.js";
30
- import"./index-jtm86byd.js";
31
- import"./index-6pz1j0zj.js";
29
+ import"./index-47qxw1tz.js";
30
+ import"./index-mxvakhsm.js";
32
31
  import"./index-67546d0j.js";
33
32
  import"./index-r24hmh0q.js";
34
33
  export {
@@ -45,7 +44,6 @@ export {
45
44
  materializeCapacitorConfig,
46
45
  isPlaceholderAppId,
47
46
  getMissingAndroidReleaseSigningKeys,
48
- getAndroidLocalServerHost,
49
47
  getAdbDeviceStateIssues,
50
48
  formatIosRunFailureMessage,
51
49
  formatAndroidReleaseSigningError,
@@ -1,23 +1,23 @@
1
1
  // @bun
2
2
  import {
3
3
  CloudScript
4
- } from "./index-ybybkec0.js";
5
- import"./index-xj8bks3p.js";
6
- import"./index-e3qskyt2.js";
4
+ } from "./index-mn96y1nk.js";
5
+ import"./index-p6txr1ss.js";
6
+ import"./index-meh7mmz9.js";
7
7
  import {
8
8
  GlobalConfig
9
- } from "./index-3pgtw35d.js";
10
- import"./index-tq1y56tp.js";
9
+ } from "./index-0cj2zxbm.js";
10
+ import"./index-nx1vbtsc.js";
11
11
  import"./index-76rn3g2c.js";
12
- import"./index-cn24dcqn.js";
12
+ import"./index-exhpp31y.js";
13
13
  import {
14
14
  Workspace,
15
15
  command
16
- } from "./index-jk9q6pse.js";
16
+ } from "./index-jkynkyfk.js";
17
17
  import"./index-fgc8r6dj.js";
18
18
  import"./index-bjpxzr6s.js";
19
- import"./index-jtm86byd.js";
20
- import"./index-6pz1j0zj.js";
19
+ import"./index-47qxw1tz.js";
20
+ import"./index-mxvakhsm.js";
21
21
  import"./index-46tjzh6s.js";
22
22
  import"./index-67546d0j.js";
23
23
  import"./index-r24hmh0q.js";
@@ -1,26 +1,26 @@
1
1
  // @bun
2
2
  import {
3
3
  ContextScript
4
- } from "./index-1s71fwrq.js";
5
- import"./index-b978vs2e.js";
6
- import"./index-csqk9z5j.js";
7
- import"./index-r4w8cry5.js";
8
- import"./index-vhv3xnjz.js";
9
- import"./index-hjzr8ja1.js";
10
- import"./index-fmgbwsr5.js";
4
+ } from "./index-p69fcwe1.js";
5
+ import"./index-6npsfpxr.js";
6
+ import"./index-c94nq3c1.js";
7
+ import"./index-pfrt7qwh.js";
8
+ import"./index-60a3jfcz.js";
9
+ import"./index-hrwzz2wf.js";
10
+ import"./index-wrrb8rxk.js";
11
11
  import"./index-ss469dec.js";
12
- import"./index-y0k42k04.js";
13
- import"./index-3pgtw35d.js";
14
- import"./index-4yp06pn1.js";
15
- import"./index-4zgs8g1w.js";
16
- import"./index-xwgm94zr.js";
12
+ import"./index-mterpj70.js";
13
+ import"./index-0cj2zxbm.js";
14
+ import"./index-jn9vfpwz.js";
15
+ import"./index-m4kpmmtj.js";
16
+ import"./index-qp089jp0.js";
17
17
  import"./index-76rn3g2c.js";
18
18
  import {
19
19
  Workspace,
20
20
  command
21
- } from "./index-jk9q6pse.js";
22
- import"./index-jtm86byd.js";
23
- import"./index-6pz1j0zj.js";
21
+ } from "./index-jkynkyfk.js";
22
+ import"./index-47qxw1tz.js";
23
+ import"./index-mxvakhsm.js";
24
24
  import"./index-xys926f2.js";
25
25
  import"./index-67546d0j.js";
26
26
  import"./index-r24hmh0q.js";
@@ -1,20 +1,20 @@
1
1
  // @bun
2
2
  import {
3
3
  AiSession
4
- } from "./index-3pgtw35d.js";
4
+ } from "./index-0cj2zxbm.js";
5
5
  import {
6
6
  Prompter
7
- } from "./index-xwgm94zr.js";
7
+ } from "./index-qp089jp0.js";
8
8
  import {
9
9
  Workspace,
10
10
  command,
11
11
  runner,
12
12
  script
13
- } from "./index-jk9q6pse.js";
13
+ } from "./index-jkynkyfk.js";
14
14
  import {
15
15
  getDirname
16
- } from "./index-jtm86byd.js";
17
- import"./index-6pz1j0zj.js";
16
+ } from "./index-47qxw1tz.js";
17
+ import"./index-mxvakhsm.js";
18
18
  import {
19
19
  FileSys
20
20
  } from "./index-67546d0j.js";
@@ -0,0 +1,31 @@
1
+ {
2
+ "title": "SSR Rule",
3
+ "description": "Akan SSR-first rendering rules — keep JSX on the server and justify every client boundary",
4
+ "group": "core",
5
+ "codegenPriority": 3,
6
+ "deprecated": false,
7
+ "scans": [
8
+ {
9
+ "type": "example",
10
+ "description": "Client boundary shells that render server children untouched",
11
+ "path": "{apps,libs}/*/ui/**/*.tsx",
12
+ "sample": 4
13
+ },
14
+ {
15
+ "type": "example",
16
+ "description": "Module UI split across client Zone/Template/Util and server Unit/View",
17
+ "path": "{apps,libs}/*/lib/*/*.tsx",
18
+ "sample": 4
19
+ }
20
+ ],
21
+ "update": {
22
+ "filePath": "./ssrRule.instruction.md",
23
+ "contents": ["purpose", "when a client boundary is earned", "quality rules", "implementation playbook"],
24
+ "rules": [
25
+ "Use current Akan APIs only",
26
+ "Keep output parseable for codegen",
27
+ "Name the akan.ssr.* rule ids the quality scanner emits",
28
+ "Ground every pattern in a file that exists in the workspace"
29
+ ]
30
+ }
31
+ }
@@ -0,0 +1,108 @@
1
+ # Akan SSR Rule Guideline
2
+
3
+ ## Purpose
4
+ Use this when writing or reviewing any `.tsx` file. Akan is SSR-first: an element rendered on the server ships as
5
+ HTML and costs nothing to hydrate, while an element behind `"use client"` ships twice — as markup and as bundled JS
6
+ that re-runs in the browser. The boundary question is never "may this file be client", it is **how little ends up
7
+ on the client side**.
8
+
9
+ ## When A Client Boundary Is Earned
10
+ - A component earns `"use client"` only by using a client-only capability: a React hook, a JSX event handler, the
11
+ store (`st.use.*` / `st.do.*`), a browser global (`window`, `document`, `navigator`, `localStorage`, …), or a
12
+ client-only third-party package.
13
+ - Rendering markup, mapping over data, reading a route param, and calling `l()` are server work. `usePage()` and
14
+ `getSelf()` are legal in server components and never force a boundary.
15
+ - In domain UI the file role fixes the directive: `Template`, `Zone`, and `Util` are client; `Unit` and `View` are
16
+ server. That decides *where* the boundary sits, not how much markup sits behind it — a `Zone` that hoards markup
17
+ is still an SSR regression.
18
+ - Measure with `akan quality ssr` (`--format json` for tooling). It reports the server render share per app and
19
+ lib as server-rendered JSX elements over total. Treat **50% server share as the floor** and a falling share as a
20
+ regression to justify or revert.
21
+
22
+ ## Quality Rules
23
+ - `akan.ssr.unnecessary-use-client` — the directive is present but nothing in the file needs it. Delete it. If the
24
+ file exists only to wrap one client child, drop the wrapper and use the child directly.
25
+ - `akan.ssr.client-static-component` — a component inside a client file renders real markup with zero client-only
26
+ capability. Move it to a server file: a `<Model>.Unit.tsx` / `<Model>.View.tsx` for a module, or a `ui/` file with
27
+ no directive.
28
+ - `akan.ssr.client-static-markup` — a large subtree wraps one or two interactive touches. Keep the interactive
29
+ element in the client component and hoist the static subtree into a server component received as `children`.
30
+ - `akan.ssr.client-mount-load` — a `useEffect(…, [])` loads server data. Fetch it in the route and pass the
31
+ init/view object down.
32
+ - `akan.ssr.module-missing-server-view` — a module renders only from `Template`/`Zone`/`Util` and declares no
33
+ `Unit`/`View`. Add them and have the `Zone` delegate.
34
+ - `akan.ssr.template-client-state` — a `Template` holds form state in `useState`. Bind to the store instead:
35
+ `value={xForm.field}` with `onChange={st.do.setFieldOnX}`.
36
+
37
+ A third-party client package and the `ui/<Folder>/index_.tsx` + `lazy()` boundary both justify the directive and are
38
+ not flagged. Interaction-driven `fetch.*` inside an `onClick` is not flagged either — only mount-time loads are,
39
+ because those are the ones the server could have performed.
40
+
41
+ ## Implementation Playbook
42
+
43
+ **① Wrap the interaction, not the UI.** The smallest useful client component adds one behaviour and renders
44
+ `children` untouched, so the markup inside never reaches the bundle.
45
+
46
+ ```tsx
47
+ "use client";
48
+ export const ClickWrapper = ({ children, onPick }: ClickWrapperProps) => <div onClick={onPick}>{children}</div>;
49
+ ```
50
+
51
+ `libs/shared/ui/Only/User.tsx` is the same shape: it reads auth state on the client and returns `{children}`.
52
+
53
+ **② Split compound components so panels stay on the server.** A tab, accordion, or disclosure needs client state
54
+ only for *which* part is visible, never for what the parts contain. `Tab` / `Tab.Menus` / `Tab.Menu` / `Tab.Panel`
55
+ in `akanjs/ui` is the reference: only the provider and menu hold state, and `<Tab.Panel>` renders its children
56
+ as-is, so a server `Unit`/`View` passed in stays server-rendered.
57
+
58
+ ```tsx
59
+ <Tab defaultMenu="detail">
60
+ <Tab.Menus>
61
+ <Tab.Menu menu="detail">{l("post.detail")}</Tab.Menu>
62
+ <Tab.Menu menu="history">{l("post.history")}</Tab.Menu>
63
+ </Tab.Menus>
64
+ <Tab.Panel menu="detail">
65
+ <Post.View.General post={post} />
66
+ </Tab.Panel>
67
+ </Tab>
68
+ ```
69
+
70
+ Never collapse this into one client file with a mode `useState` and every panel body inlined.
71
+
72
+ **③ Sync state instead of fetching it.** A server component cannot hold state, so render the initial data on the
73
+ server and hand it across as a serializable object. The route calls `fetch.initXInY(...)` / `fetch.viewX(...)` and
74
+ passes the result into a `Zone`; `Load.Units` / `Load.View` hydrate the store from it. A `useEffect(…, [])` that
75
+ fetches on mount renders an empty shell, hydrates, then round-trips for data the server already had.
76
+
77
+ **④ Push the boundary down to the leaf that needs it.** A store-reading `Zone` should hold zero markup and delegate
78
+ to a server `View` — `User.Zone.Self` is one line, `st.use.self()` into `<User.View.General user={self} />`, so the
79
+ whole detail surface renders server-side wherever a route uses the `View` directly.
80
+
81
+ **⑤ Hand the promise across, not the awaited value.** `ClientInit` / `ClientView` are `PromiseOrObject<T>`, so a
82
+ route may pass an unawaited `fetch.initX(...)` and `Load.*` resolves it behind a skeleton. `await` blocks the shell
83
+ for data the page needs immediately; the promise streams the rest. Independent fetches still go through one
84
+ `Promise.all`.
85
+
86
+ **⑥ Use named `ReactNode` slots, not just `children`.** `Layout.Navbar` accepts `title`, `back`, `left`, `right`,
87
+ and `children`, so a client shell composes server-rendered content in five places instead of absorbing it.
88
+
89
+ **⑦ Let the server do the derived work.** Display and predicate logic belongs on `Light<Model>` (`isNew()`,
90
+ `canWrite(user?)`, `formatTimes()`), and enum→class lookups belong in a module-scope `as const` map. Both sides call
91
+ the same method, so a client component that exists only to compute a label is markup in the wrong place.
92
+
93
+ **⑧ Gate auth on the server.** `getSelf({ unauthorize: "/signin" })` in `_layout.tsx` redirects before any HTML is
94
+ sent. A client-side check costs a hydration round-trip and flashes the wrong UI first.
95
+
96
+ **⑨ Prefer CSS over client state for pure visibility.** A `data-*` attribute plus `group-data-[…]` variants (see
97
+ `libs/util/ui/Grid/*`) or `<details>`/`<summary>` keeps both branches server-rendered. Reach for `useState` when the
98
+ state is real, not when a Tailwind variant would do.
99
+
100
+ **⑩ Keep the heavy island out of the first load.** A large client-only widget goes behind the
101
+ `ui/<Folder>/index_.tsx` + `lazy()` pair so the server renders the page around it.
102
+
103
+ ## Review Checklist
104
+ - Does every `"use client"` in the diff name a capability that requires it?
105
+ - Does each client component hold only interaction, with markup delegated to `children`, a slot prop, or a
106
+ `Unit`/`View`?
107
+ - Is initial data fetched in the route and passed as `init`/`view`, with no mount-only load effect?
108
+ - Did `akan quality ssr` hold or improve the server share for every touched app and lib?
@@ -8,7 +8,7 @@ import {
8
8
  DevGeneratedIndexSync,
9
9
  GraphClientEntryDiscovery,
10
10
  RouteClientBuilder
11
- } from "./index-rh16j3c7.js";
11
+ } from "./index-j9cxndc0.js";
12
12
  import {
13
13
  HmrWatcher,
14
14
  WatchRootResolver
@@ -16,8 +16,8 @@ import {
16
16
  import {
17
17
  AppExecutor,
18
18
  WorkspaceExecutor
19
- } from "./index-jtm86byd.js";
20
- import"./index-6pz1j0zj.js";
19
+ } from "./index-47qxw1tz.js";
20
+ import"./index-mxvakhsm.js";
21
21
  import"./index-67546d0j.js";
22
22
  import"./index-r24hmh0q.js";
23
23
 
@@ -1,7 +1,7 @@
1
1
  // @bun
2
2
  import {
3
3
  Spinner
4
- } from "./index-6pz1j0zj.js";
4
+ } from "./index-mxvakhsm.js";
5
5
  import {
6
6
  FileSys
7
7
  } from "./index-67546d0j.js";
@@ -9,7 +9,7 @@ import {
9
9
  createTsconfigPackageResolver,
10
10
  rewriteBarrelImports,
11
11
  transformUseClient
12
- } from "./index-rh16j3c7.js";
12
+ } from "./index-j9cxndc0.js";
13
13
  import {
14
14
  __require
15
15
  } from "./index-r24hmh0q.js";
@@ -415,11 +415,21 @@ class AllRoutesBuilder {
415
415
  const seedIndex = computeRouteSeedIndex(pageEntries);
416
416
  this.#app.verbose(`[build-all] discovered ${seedIndex.entries.length} routes`);
417
417
  this.#discovery = await GraphClientEntryDiscovery.create(this.#app);
418
+ const allEntries = [];
419
+ const seen = new Set;
418
420
  for (const entry of seedIndex.entries) {
419
421
  const seeds = Array.from(new Set([...seedIndex.globalLayoutFiles, ...entry.seeds]));
420
- const delta = await this.#buildRoute(entry.routeId, seeds);
421
- this.#mergeRoute(entry.routeId, delta);
422
+ for (const discovered of await this.#discovery.discover(seeds)) {
423
+ if (seen.has(discovered))
424
+ continue;
425
+ seen.add(discovered);
426
+ allEntries.push(discovered);
427
+ }
428
+ this.#routeIds.push(entry.routeId);
422
429
  }
430
+ this.#app.verbose(`[build-all] ${allEntries.length} client entries across ${this.#routeIds.length} routes`);
431
+ const delta = await this.#buildEntries(allEntries);
432
+ this.#mergeDelta(delta);
423
433
  this.#merged.knownEntries = Array.from(this.#knownSet);
424
434
  const manifest = {
425
435
  routeIds: this.#routeIds,
@@ -433,23 +443,30 @@ class AllRoutesBuilder {
433
443
  this.#app.verbose(`[build-all] wrote ${manifestPath} routes=${this.#routeIds.length} entries=${this.#merged.knownEntries.length}`);
434
444
  return { manifest, manifestPath, seedIndex };
435
445
  }
436
- async#buildRoute(routeId, seeds) {
446
+ async#buildEntries(entries) {
437
447
  if (!this.#discovery)
438
448
  throw new Error("[build-all] client entry discovery is not initialized");
449
+ if (entries.length === 0)
450
+ return {
451
+ manifestDelta: {},
452
+ ssrManifestDelta: { moduleLoading: null, moduleMap: {} },
453
+ newEntries: [],
454
+ clientDeps: []
455
+ };
439
456
  const started = Date.now();
440
457
  const delta = await new RouteClientBuilder({
441
458
  app: this.#app,
442
- routeId,
443
- seeds,
459
+ seeds: [],
460
+ entries,
444
461
  artifact: this.#artifact,
445
462
  knownEntries: this.#knownSet,
446
463
  discovery: this.#discovery,
447
464
  command: this.#command
448
465
  }).build();
449
- this.#app.verbose(`[build-all] ${routeId} +${delta.newEntries.length} entries (${Date.now() - started}ms)`);
466
+ this.#app.verbose(`[build-all] bundled ${delta.newEntries.length} entries (${Date.now() - started}ms)`);
450
467
  return delta;
451
468
  }
452
- #mergeRoute(routeId, delta) {
469
+ #mergeDelta(delta) {
453
470
  for (const [key, row] of Object.entries(delta.manifestDelta))
454
471
  this.#merged.clientManifest[key] = row;
455
472
  for (const [url, byName] of Object.entries(delta.ssrManifestDelta.moduleMap)) {
@@ -457,7 +474,6 @@ class AllRoutesBuilder {
457
474
  }
458
475
  for (const abs of delta.newEntries)
459
476
  this.#knownSet.add(abs);
460
- this.#routeIds.push(routeId);
461
477
  }
462
478
  }
463
479
 
@@ -794,10 +810,10 @@ import { compile } from "tailwindcss";
794
810
 
795
811
  // pkgs/@akanjs/devkit/frontendBuild/cssCandidateCache.ts
796
812
  import { stat } from "fs/promises";
797
- var CANDIDATE_RE = /-?(?:[\w@]|\[[^\]]+\])[\w:/.-]*(?:\[[^\]]+\][\w:/.-]*)*/g;
813
+ var CANDIDATE_RE = /-?[\w@][\w:/.-]*(?:\[[^\]]+\][\w:/.-]*)*/g;
798
814
 
799
815
  class CssCandidateCache {
800
- static #version = 2;
816
+ static #version = 1;
801
817
  #path;
802
818
  #entries = new Map;
803
819
  #dirty = false;
@@ -2025,14 +2041,21 @@ function loaderFor2(absPath) {
2025
2041
  // pkgs/@akanjs/devkit/frontendBuild/precompressArtifacts.ts
2026
2042
  import fs2 from "fs";
2027
2043
  import path12 from "path";
2044
+ import zlib from "zlib";
2028
2045
  var COMPRESSIBLE_EXTS = new Set([".css", ".html", ".js", ".json", ".svg"]);
2029
2046
  var MIN_COMPRESS_BYTES = 1024;
2047
+ var BROTLI_QUALITY_BY_EXT = { ".css": 11 };
2048
+ var DEFAULT_BROTLI_QUALITY = 9;
2049
+ var GZIP_LEVEL = 9;
2030
2050
  async function precompressArtifacts(app) {
2031
- const roots = [path12.join(app.dist.cwdPath, ".akan/artifact/client")];
2032
- const result = { files: 0, inputBytes: 0, outputBytes: 0 };
2051
+ const roots = [
2052
+ path12.join(app.dist.cwdPath, ".akan/artifact/client"),
2053
+ path12.join(app.dist.cwdPath, ".akan/artifact/styles")
2054
+ ];
2055
+ const result = { files: 0, inputBytes: 0, outputBytes: 0, brotliBytes: 0 };
2033
2056
  await Promise.all(roots.map((root) => precompressRoot(root, result)));
2034
2057
  if (result.files > 0) {
2035
- app.verbose(`[precompress] wrote ${result.files} gzip sidecars (${formatBytes(result.inputBytes)} -> ${formatBytes(result.outputBytes)})`);
2058
+ app.verbose(`[precompress] wrote ${result.files} sidecars (${formatBytes(result.inputBytes)} -> gzip ${formatBytes(result.outputBytes)} / br ${formatBytes(result.brotliBytes)})`);
2036
2059
  }
2037
2060
  return result;
2038
2061
  }
@@ -2044,15 +2067,27 @@ async function precompressRoot(root, result) {
2044
2067
  if (!await shouldPrecompress(filePath))
2045
2068
  continue;
2046
2069
  const bytes = await Bun.file(filePath).bytes();
2047
- const gz = Bun.gzipSync(toArrayBuffer(bytes));
2048
- await Bun.write(`${filePath}.gz`, gz);
2070
+ const buffer = toArrayBuffer(bytes);
2071
+ const gz = Bun.gzipSync(buffer, { level: GZIP_LEVEL });
2072
+ const br = brotliCompress(buffer, path12.extname(filePath).toLowerCase());
2073
+ await Promise.all([Bun.write(`${filePath}.gz`, gz), Bun.write(`${filePath}.br`, br)]);
2049
2074
  result.files += 1;
2050
2075
  result.inputBytes += bytes.byteLength;
2051
2076
  result.outputBytes += gz.byteLength;
2077
+ result.brotliBytes += br.byteLength;
2052
2078
  }
2053
2079
  }
2080
+ function brotliCompress(buffer, ext) {
2081
+ const quality = BROTLI_QUALITY_BY_EXT[ext] ?? DEFAULT_BROTLI_QUALITY;
2082
+ return zlib.brotliCompressSync(buffer, {
2083
+ params: {
2084
+ [zlib.constants.BROTLI_PARAM_QUALITY]: quality,
2085
+ [zlib.constants.BROTLI_PARAM_SIZE_HINT]: buffer.byteLength
2086
+ }
2087
+ });
2088
+ }
2054
2089
  async function shouldPrecompress(filePath) {
2055
- if (filePath.endsWith(".gz"))
2090
+ if (filePath.endsWith(".gz") || filePath.endsWith(".br"))
2056
2091
  return false;
2057
2092
  if (!COMPRESSIBLE_EXTS.has(path12.extname(filePath).toLowerCase()))
2058
2093
  return false;