@akanjs/cli 3.0.0-alpha.3 → 3.0.0-alpha.4
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/.build-stamp +1 -1
- package/{abstractCompactor-e5yq60qz.js → abstractCompactor-rk6twzvj.js} +2 -2
- package/{agent.command-yv00p0s0.js → agent.command-jfz8yfwy.js} +7 -7
- package/{application.command-0qsczf4k.js → application.command-mxyqtd6p.js} +5 -5
- package/{applicationBuildRunner-ct2pxyvw.js → applicationBuildRunner-cwk52sb7.js} +2 -2
- package/{applicationReleasePackager-gg8ccbeh.js → applicationReleasePackager-yrrhbkv3.js} +1 -1
- package/buildBatch.proc.js +3 -3
- package/{capacitorApp-4r9sm3qg.js → capacitorApp-j357ak1g.js} +5 -3
- package/{cloud.command-dxfv18sa.js → cloud.command-f87dwe52.js} +9 -9
- package/{context.command-8d9e3k6b.js → context.command-z5bf8bn7.js} +15 -15
- package/{guideline.command-3jbkxzh6.js → guideline.command-1m1ctar3.js} +5 -5
- package/incrementalBuilder.proc.js +2 -2
- package/{index-1pvex4az.js → index-1s71fwrq.js} +11 -11
- package/{index-0cj2zxbm.js → index-3pgtw35d.js} +1 -1
- package/{index-a00jg8x8.js → index-4yp06pn1.js} +2 -2
- package/{index-rzwcpcc3.js → index-4zgs8g1w.js} +1 -1
- package/{index-mxvakhsm.js → index-6pz1j0zj.js} +2 -6
- package/{index-mr5s7mqd.js → index-b978vs2e.js} +3 -3
- package/{index-pyt2b4nw.js → index-cap2k2ch.js} +2 -2
- package/{index-7c6k4yvg.js → index-cn24dcqn.js} +2 -2
- package/{index-2q1f5j9k.js → index-csqk9z5j.js} +3 -3
- package/{index-k95xkh4p.js → index-e3qskyt2.js} +2 -2
- package/{index-ec09pws3.js → index-fmgbwsr5.js} +4 -4
- package/{index-bbkek1bp.js → index-hjzr8ja1.js} +1 -1
- package/{index-8hsbnyes.js → index-jk9q6pse.js} +1 -1
- package/{index-hgna5rjy.js → index-jtm86byd.js} +1 -1
- package/{index-ej426mcr.js → index-n0z3f2xg.js} +4 -4
- package/{index-bekddz9q.js → index-r4w8cry5.js} +4 -4
- package/{index-e16gnn26.js → index-tq1y56tp.js} +12 -35
- package/{index-yzmhasdb.js → index-vhv3xnjz.js} +6 -6
- package/{index-zasydjxd.js → index-xj8bks3p.js} +4 -4
- package/{index-tdbas9r0.js → index-xwgm94zr.js} +1 -1
- package/{index-t3723f99.js → index-y0k42k04.js} +86 -17
- package/{index-nm6wghb6.js → index-ybybkec0.js} +6 -6
- package/index.js +19 -19
- package/{library.command-ebdj1vnm.js → library.command-pwfx8dx2.js} +4 -4
- package/{localRegistry.command-gb178ne8.js → localRegistry.command-r4zjx5py.js} +8 -8
- package/{module.command-9c1bnsx4.js → module.command-0t3ewata.js} +8 -8
- package/{package.command-t93p4jkj.js → package.command-7hmkxvpa.js} +4 -4
- package/package.json +2 -2
- package/{page.command-k3k0meh2.js → page.command-st6r4s0q.js} +4 -4
- package/{primitive.command-de6nktdt.js → primitive.command-1qkxpzsn.js} +9 -9
- package/{quality.command-z5kth2wa.js → quality.command-hy5vt1bh.js} +48 -451
- package/{repair.command-ehf2g112.js → repair.command-nakkq2zp.js} +6 -6
- package/{scalar.command-7a2jtv33.js → scalar.command-fjq272gs.js} +7 -7
- package/templates/lib/db.ts +1 -3
- package/templates/libRoot/base/baseLogic.ts +14 -0
- package/templates/libRoot/base/index.ts +8 -0
- package/templates/workspaceRoot/.cursor/rules/client-server-boundaries.mdc.template +0 -1
- package/templates/workspaceRoot/AGENTS.md.template +25 -157
- package/templates/workspaceRoot/biome.json.template +19 -0
- package/templates/workspaceRoot/docs/AI-DEVELOPMENT.md.template +4 -1
- package/{workflow.command-e0tt0vk2.js → workflow.command-jswbgk90.js} +12 -12
- package/{workspace.command-fpda0evj.js → workspace.command-h9wwcjpt.js} +21 -21
- package/guidelines/ssrRule/ssrRule.generate.json +0 -31
- package/guidelines/ssrRule/ssrRule.instruction.md +0 -108
- package/templates/workspaceRoot/.cursor/rules/ssr-first.mdc.template +0 -59
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
RepairRunner
|
|
4
|
-
} from "./index-
|
|
5
|
-
import"./index-
|
|
6
|
-
import"./index-
|
|
4
|
+
} from "./index-b978vs2e.js";
|
|
5
|
+
import"./index-4yp06pn1.js";
|
|
6
|
+
import"./index-4zgs8g1w.js";
|
|
7
7
|
import {
|
|
8
8
|
Workspace,
|
|
9
9
|
command,
|
|
10
10
|
script
|
|
11
|
-
} from "./index-
|
|
12
|
-
import"./index-
|
|
13
|
-
import"./index-
|
|
11
|
+
} from "./index-jk9q6pse.js";
|
|
12
|
+
import"./index-jtm86byd.js";
|
|
13
|
+
import"./index-6pz1j0zj.js";
|
|
14
14
|
import"./index-xys926f2.js";
|
|
15
15
|
import"./index-67546d0j.js";
|
|
16
16
|
import"./index-r24hmh0q.js";
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
ScalarScript
|
|
4
|
-
} from "./index-
|
|
4
|
+
} from "./index-fmgbwsr5.js";
|
|
5
5
|
import"./index-ss469dec.js";
|
|
6
|
-
import"./index-
|
|
6
|
+
import"./index-3pgtw35d.js";
|
|
7
7
|
import {
|
|
8
8
|
renderPrimitiveReport
|
|
9
|
-
} from "./index-
|
|
10
|
-
import"./index-
|
|
9
|
+
} from "./index-4zgs8g1w.js";
|
|
10
|
+
import"./index-xwgm94zr.js";
|
|
11
11
|
import {
|
|
12
12
|
Sys,
|
|
13
13
|
command
|
|
14
|
-
} from "./index-
|
|
15
|
-
import"./index-
|
|
16
|
-
import"./index-
|
|
14
|
+
} from "./index-jk9q6pse.js";
|
|
15
|
+
import"./index-jtm86byd.js";
|
|
16
|
+
import"./index-6pz1j0zj.js";
|
|
17
17
|
import"./index-67546d0j.js";
|
|
18
18
|
import"./index-r24hmh0q.js";
|
|
19
19
|
|
package/templates/lib/db.ts
CHANGED
|
@@ -16,9 +16,7 @@ ${databaseModules.map((module) => `import * as ${module}Db from "./${module}/${m
|
|
|
16
16
|
${libs.map((lib) => `export { db as ${lib} } from "@libs/${lib}/server";`).join("\n")}
|
|
17
17
|
|
|
18
18
|
${databaseModules.map((module) => `class ${capitalize(module)}Input extends by(cnst.${capitalize(module)}Input) {}`).join("\n")}
|
|
19
|
-
${databaseModules.map((module) => `class ${capitalize(module)}Insight extends by(cnst.${capitalize(module)}Insight) {}`).join("\n")}
|
|
20
19
|
${databaseModules.length ? `export type { ${databaseModules.map((module) => `${capitalize(module)}Input`).join(", ")} };` : ""}
|
|
21
|
-
${databaseModules.length ? `export type { ${databaseModules.map((module) => `${capitalize(module)}Insight`).join(", ")} };` : ""}
|
|
22
20
|
|
|
23
21
|
${scalarModules.map((module) => `export type * from "./__scalar/${module}/${module}.document";`).join("\n")}
|
|
24
22
|
${databaseModules.map((module) => `export type * from "./${module}/${module}.document";`).join("\n")}
|
|
@@ -26,7 +24,7 @@ ${databaseModules.map((module) => `export type * from "./${module}/${module}.doc
|
|
|
26
24
|
${databaseModules
|
|
27
25
|
.map((module) => {
|
|
28
26
|
const names = { Module: module.charAt(0).toUpperCase() + module.slice(1) };
|
|
29
|
-
return `export const ${module} = DatabaseRegistry.buildModel("${module}" as const, ${names.Module}Input, ${module}Db.${names.Module}, ${module}Db.${names.Module}Model, cnst.${names.Module},
|
|
27
|
+
return `export const ${module} = DatabaseRegistry.buildModel("${module}" as const, ${names.Module}Input, ${module}Db.${names.Module}, ${module}Db.${names.Module}Model, cnst.${names.Module}, cnst.${names.Module}Insight, ${module}Db.${names.Module}Filter);`;
|
|
30
28
|
})
|
|
31
29
|
.join("\n")}
|
|
32
30
|
${scalarModules.map((module) => `export const ${module} = DatabaseRegistry.buildScalar("${module}" as const, ${capitalize(module)});`).join("\n")}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
interface Dict {
|
|
2
|
+
libName: string;
|
|
3
|
+
}
|
|
4
|
+
export default function getContent(scanInfo: null, dict: Dict) {
|
|
5
|
+
return `
|
|
6
|
+
// 현 디렉토리에서는 프로젝트 내 전체적으로 사용되는 로직을 구현하며, 따라서 백/프론트 비의존적인 pure js 코드만 구현 가능합니다.
|
|
7
|
+
// common폴더와 다른점은, base 코드는 시스템 전체에서 import되어 사용되므로, 가장 핵심적인 로직과 추상화된 기능만 구현해야합니다.
|
|
8
|
+
// @${dict.libName}/base에서는 서버/클라이언트 관련 라이브러리(@*/server, @*/server, @*/client, @*/client)를 모두 import할 수 없으며, @*/base 라이브러리만 import 가능합니다.
|
|
9
|
+
|
|
10
|
+
export const someBaseLogic = () => {
|
|
11
|
+
//
|
|
12
|
+
};
|
|
13
|
+
`;
|
|
14
|
+
}
|
|
@@ -15,4 +15,3 @@ alwaysApply: false
|
|
|
15
15
|
- In domain UI the boundary is mechanical, not a judgment call: `Template`, `Zone`, and `Util` are always client components with `"use client"` on line 1; `Unit` and `View` are always server components and never carry the directive. `usePage()` is still legal in server files.
|
|
16
16
|
- Preserve established domain file roles such as `.document.ts`, `.service.ts`, `.store.ts`, `.constant.ts`, and `.client.ts`.
|
|
17
17
|
- When unsure, inspect nearby files in the same app or package before introducing a new boundary pattern.
|
|
18
|
-
- The boundary is only half the rule: how *little* ends up on the client is the other half. See `ssr-first.mdc`.
|
|
@@ -74,12 +74,11 @@ Use the local module shape before adding a new abstraction.
|
|
|
74
74
|
9. Keep page and layout files server-oriented: route-level `usePage`, headers, static layout, and `akanjs/ui` `Tab` composition belong there.
|
|
75
75
|
10. Put interactive loading, submit/import actions, local form state, and `router.push` in client `Util`, `Template`, or `Zone` components.
|
|
76
76
|
11. Prefer `Tab` for static mode selection instead of extra `useState`; render one focused client component inside each `Tab.Panel`.
|
|
77
|
-
12.
|
|
78
|
-
13.
|
|
79
|
-
14.
|
|
80
|
-
15.
|
|
81
|
-
16.
|
|
82
|
-
17. Run the smallest relevant verification command after changes. After touching any `.tsx`, that includes `akan quality ssr`.
|
|
77
|
+
12. Keep server-only logic out of client surfaces and client-only code out of server imports.
|
|
78
|
+
13. Treat `AKAN_PUBLIC_*` env vars as public. Never put secrets in them.
|
|
79
|
+
14. Add or update tests when behavior, contracts, or CLI output changes.
|
|
80
|
+
15. Update `*.abstract.md` when business invariants, workflows, or public behavior change.
|
|
81
|
+
16. Run the smallest relevant verification command after changes.
|
|
83
82
|
|
|
84
83
|
## Common Commands
|
|
85
84
|
|
|
@@ -112,11 +111,7 @@ akan typecheck <%= appName %>
|
|
|
112
111
|
# 3. Test — Run the test code (lib/*/*.signal.test.ts or others)
|
|
113
112
|
akan test <%= appName %>
|
|
114
113
|
|
|
115
|
-
# 4.
|
|
116
|
-
# per app/lib and flags client code that should render on the server.
|
|
117
|
-
akan quality ssr
|
|
118
|
-
|
|
119
|
-
# 5. Full production build — bundles the app, runs all type/lint checks combined
|
|
114
|
+
# 4. Full production build — bundles the app, runs all type/lint checks combined
|
|
120
115
|
akan build <%= appName %>
|
|
121
116
|
```
|
|
122
117
|
|
|
@@ -133,8 +128,6 @@ akan create-scalar <scalar-name> <%= appName %> # Add a scalar module (
|
|
|
133
128
|
akan create-service <service-name> <%= appName %> # Add a service module (lib/_<service-name>/)
|
|
134
129
|
akan test <%= appName %> # Run the test code (lib/*/*.signal.test.ts or others)
|
|
135
130
|
akan lint <%= appName %> # Lint only (no typecheck)
|
|
136
|
-
akan quality scan # All code-quality warnings + SSR balance
|
|
137
|
-
akan quality ssr # SSR balance and client-boundary warnings only
|
|
138
131
|
```
|
|
139
132
|
|
|
140
133
|
**CLI argument conventions.** Two argument styles, and mixing them up is a common mistake:
|
|
@@ -247,7 +240,7 @@ final fallback when no CLI command covers the change.
|
|
|
247
240
|
| Add a reusable value type (e.g., Address, WorkHistory) | `lib/__scalar/<type>/` → constant, dictionary, abstract | `akan sync <name>` |
|
|
248
241
|
| Create a new URL-visitable page | `page/` → `_index.tsx`, `_layout.tsx`, `[param]/_index.tsx` | Rebuild (akan start auto-detects) |
|
|
249
242
|
| Change the app color theme / design tokens | `apps/<app>/page/styles.css` → override the semantic token values under `:root, [data-theme="dark"]` and `[data-theme="light"]` (`--primary`, `--background`, `--foreground`, …) | akan start hot-reloads |
|
|
250
|
-
| Add a form or reusable UI component | `ui/` → PascalCase `.tsx
|
|
243
|
+
| Add a form or reusable UI component | `ui/` → PascalCase `.tsx` with `"use client"` if needed | `akan sync <name>` |
|
|
251
244
|
| Add a React hook or browser helper | `webkit/` → camelCase `.ts` with `"use client"` | `akan sync <name>` |
|
|
252
245
|
| Add a server-only guard, middleware, or adaptor | `srvkit/` → PascalCase `.ts` | `akan sync <name>` |
|
|
253
246
|
| Add a pure helper (no DOM, no server API) | `common/` → camelCase `.ts` | `akan sync <name>` |
|
|
@@ -261,127 +254,14 @@ final fallback when no CLI command covers the change.
|
|
|
261
254
|
| Use JS `#private` in `*.constant.ts`, `*.document.ts`, `*.service.ts`, or `*.store.ts` | `no-js-private-class-method.grit` bans `#private` in exactly those four file suffixes. The rule is scoped by file path, not class shape — `#private` stays the house style under `srvkit/`, including `adapt()` classes. | `private _methodName()` in those four files; `#methodName()` in `srvkit/` |
|
|
262
255
|
| Use `console.log()` | Biome lint forbids `console.log`. Only `console.error`, `console.info`, `console.warn` are allowed. | Use one of the three allowed console methods, or `this.logger.*` / `new Logger("ClassName")` on the server |
|
|
263
256
|
| `throw new Error("...")` | `no-throw-raw-error.grit` bans raw errors outside tests, `*.constant.ts`, and `common/`. Raw errors carry no dictionary key, so they cannot be localized or toasted. | `throw new Err("task.error.<key>")` plus an `[en, ko]` entry in the module dictionary's `.error({})` |
|
|
264
|
-
| Hand-order Tailwind classes, or reorder them to "fix" a diff | `useSortedClasses` is an error and also sorts the string arguments to `
|
|
257
|
+
| Hand-order Tailwind classes, or reorder them to "fix" a diff | `useSortedClasses` is an error and also sorts the string arguments to `cn()`. Sorter output like `font-bold text-2xl` looks wrong but is correct. | Write classes in any order and let `akan lint` sort them |
|
|
258
|
+
| A colour outside the semantic vocabulary — `bg-red-500`, `bg-[#3b82f6]`, `btn-primary`, `text-base-content`, `style={{ color: "#fff" }}` | The theme closes the vocabulary, so these produce **no CSS at all** — the element renders unstyled, and without the lint rules there is no error and no warning either. Raw-palette, arbitrary-colour, daisyUI-legacy and inline-colour rules all catch this. | Semantic tokens: `bg-primary`, `text-foreground/70`, `border-border`. A genuinely fixed colour takes a `// biome-ignore lint/plugin: <reason>` |
|
|
265
259
|
| `import` a third-party package inside a page, a barrel, or a module file | `no-import-external-library.grit` covers `page/**`, all barrels, and every `*.{constant,dictionary,document,service,signal,store}.ts` and `*.{Template,Unit,Util,View,Zone}.tsx`. | Re-export the symbol from a one-line shim in `base/`, `webkit/`, or `ui/` first, then import that |
|
|
266
260
|
| Import server APIs (`fs`, `Bun`, `process.env`) in `ui/`, `webkit/`, or `common/` | Server-only imports in client code cause build failures. | Keep server dependencies in `lib/`, `srvkit/`, or `private/` only |
|
|
267
261
|
| Skip running `akan sync` after deleting a file | Deleted files remain referenced in barrel exports, causing import errors everywhere. | Run `akan sync <name>` after every file add, remove, or rename |
|
|
268
262
|
| Use "use client" or `useState`/`useEffect` in pages/*.tsx, *.Unit.tsx, and *.View.tsx files | Server code cannot use React hooks. Wrap in a separate `"use client"` component. | Move hook logic to `webkit/` or a `"use client"` UI component |
|
|
269
263
|
| Use `<a>` tag for internal navigation between pages | Akan.js uses `<Link>` from `akanjs/ui` for client-side navigation — avoids full page reloads. | `import { Link } from "akanjs/ui"` and use `<Link href="/task">...</Link>` |
|
|
270
264
|
| Name a custom `Endpoint`/`Slice` like a generated CRUD op — `create<Model>`, `update<Model>`, `remove<Model>`, `view<Model>`, `edit<Model>`, `merge<Model>` | These names are already auto-generated. A collision can pass sync/typecheck/build and only fail at runtime. | Pick a distinct verb, e.g. `startTask`/`archiveTask`, never `createTask` for a custom endpoint |
|
|
271
|
-
| Add `"use client"` to a file that renders markup but uses no hook, event handler, store, or browser API | The directive is a cost, not a formality: everything behind it ships twice, as HTML and as JS that re-runs in the browser. | Delete the directive. `akan quality ssr` flags this as `akan.ssr.unnecessary-use-client` |
|
|
272
|
-
| Put a large static subtree inside a client component that only needs one `onClick` | The whole subtree lands in the bundle for the sake of one handler. | Keep the handler in a small client shell and pass the static markup in as `children`, or move it to a `Unit`/`View` |
|
|
273
|
-
| Load server data with `useEffect(() => { void st.do.initTaskInTodo(); }, [])` | The page renders an empty shell, hydrates, then round-trips for data the server already had. | `await fetch.initTaskInTodo()` in the page and pass `init` to the `Zone` |
|
|
274
|
-
|
|
275
|
-
## SSR First — Server Rendering Is The Default
|
|
276
|
-
|
|
277
|
-
Akan is SSR-first. Every JSX element that renders on the server ships as HTML and costs nothing to hydrate; every
|
|
278
|
-
element behind `"use client"` ships twice — as markup and as bundled JS that must re-run in the browser. The
|
|
279
|
-
boundary question is not which file *may* be client, it is **how little** ends up on the client side.
|
|
280
|
-
|
|
281
|
-
**The default is server. `"use client"` is a cost you justify per component, not a habit.** A component earns the
|
|
282
|
-
directive only by using a client-only capability: a React hook, a JSX event handler, the store (`st.use.*` /
|
|
283
|
-
`st.do.*`), a browser global, or a client-only third-party package. Rendering markup, mapping over data, reading a
|
|
284
|
-
route param, and calling `l()` are all server work — `usePage()` and `l()` are legal in server components and never
|
|
285
|
-
force a boundary.
|
|
286
|
-
|
|
287
|
-
The file role decides *where* the boundary sits (`Template`/`Zone`/`Util` are client, `Unit`/`View` are server); it
|
|
288
|
-
does not decide how much markup sits behind it. A `Zone` that hoards markup is still an SSR regression.
|
|
289
|
-
|
|
290
|
-
Measure with `akan quality ssr` (`--format json` for tooling). It reports the server render share per app and lib —
|
|
291
|
-
server-rendered JSX elements over total — plus the warnings below. Treat **50% server share as the floor** and a
|
|
292
|
-
falling share as a regression to justify or revert.
|
|
293
|
-
|
|
294
|
-
| Rule | Means |
|
|
295
|
-
|---|---|
|
|
296
|
-
| `akan.ssr.unnecessary-use-client` | The directive is there but nothing in the file needs it. Delete it. |
|
|
297
|
-
| `akan.ssr.client-static-component` | A component in a client file renders real markup with zero client-only capability — pure server work sitting in the bundle. |
|
|
298
|
-
| `akan.ssr.client-static-markup` | A large subtree wraps one or two interactive touches. Split it: interaction stays client, markup goes server. |
|
|
299
|
-
| `akan.ssr.client-mount-load` | A `useEffect(…, [])` loads server data. The page can fetch it before the first byte. |
|
|
300
|
-
| `akan.ssr.module-missing-server-view` | A module renders only from `Template`/`Zone`/`Util` and has no `Unit`/`View` at all. |
|
|
301
|
-
| `akan.ssr.template-client-state` | A `Template` holds form state in `useState` instead of the store. |
|
|
302
|
-
|
|
303
|
-
A third-party client package and the `ui/<Folder>/index_.tsx` + `lazy()` boundary both justify the directive and are
|
|
304
|
-
not flagged. Interaction-driven `fetch.*` inside an `onClick` is not flagged either — only mount-time loads are,
|
|
305
|
-
because those are the ones the server could have performed.
|
|
306
|
-
|
|
307
|
-
### Server-Side Implementation Playbook
|
|
308
|
-
|
|
309
|
-
**① Wrap the interaction, not the UI.** The smallest useful client component adds one behaviour and renders
|
|
310
|
-
`children` untouched, so the markup inside never reaches the bundle.
|
|
311
|
-
|
|
312
|
-
```tsx
|
|
313
|
-
// apps/<%= appName %>/ui/ClickWrapper.tsx
|
|
314
|
-
"use client";
|
|
315
|
-
export const ClickWrapper = ({ children, onPick }: ClickWrapperProps) => <div onClick={onPick}>{children}</div>;
|
|
316
|
-
|
|
317
|
-
// in a server page — Task.Unit.Card stays server-rendered
|
|
318
|
-
<ClickWrapper onPick={…}>
|
|
319
|
-
<Task.Unit.Card task={task} />
|
|
320
|
-
</ClickWrapper>
|
|
321
|
-
```
|
|
322
|
-
|
|
323
|
-
**② Split compound components so panels stay on the server.** A tab, accordion, or disclosure needs client state
|
|
324
|
-
only for *which* part is visible — never for what the parts contain. `Tab` / `Tab.Menus` / `Tab.Menu` / `Tab.Panel`
|
|
325
|
-
from `akanjs/ui` is the reference shape: only the provider and menu hold state, and `<Tab.Panel>` renders its
|
|
326
|
-
children as-is, so a server `Unit`/`View` passed in stays server-rendered.
|
|
327
|
-
|
|
328
|
-
```tsx
|
|
329
|
-
<Tab defaultMenu="detail">
|
|
330
|
-
<Tab.Menus>
|
|
331
|
-
<Tab.Menu menu="detail">{l("task.detail")}</Tab.Menu>
|
|
332
|
-
<Tab.Menu menu="history">{l("task.history")}</Tab.Menu>
|
|
333
|
-
</Tab.Menus>
|
|
334
|
-
<Tab.Panel menu="detail">
|
|
335
|
-
<Task.View.General task={task} />
|
|
336
|
-
</Tab.Panel>
|
|
337
|
-
</Tab>
|
|
338
|
-
```
|
|
339
|
-
|
|
340
|
-
Never collapse this into one `"use client"` file with a mode `useState` and every panel body inlined.
|
|
341
|
-
|
|
342
|
-
**③ Sync state instead of fetching it.** A server component cannot hold state, so render the initial data on the
|
|
343
|
-
server and hand it across the boundary as a serializable object. That is what `init` / `view` props are: the page
|
|
344
|
-
calls `fetch.initTaskInTodo()` / `fetch.viewTask(id)`, passes the result into a `Zone`, and `Load.Units` /
|
|
345
|
-
`Load.View` hydrate the store from it.
|
|
346
|
-
|
|
347
|
-
```tsx
|
|
348
|
-
export default async function Page() {
|
|
349
|
-
const [{ taskInitInTodo }] = await Promise.all([fetch.initTaskInTodo()]);
|
|
350
|
-
return <Task.Zone.Card init={taskInitInTodo} sliceName="taskInTodo" />;
|
|
351
|
-
}
|
|
352
|
-
```
|
|
353
|
-
|
|
354
|
-
**④ Push the boundary down to the leaf that needs it.** When a `Zone` reads the store, it should hold *zero* markup
|
|
355
|
-
and delegate to a server `View`, so the whole detail surface renders server-side wherever a page uses the `View`
|
|
356
|
-
directly.
|
|
357
|
-
|
|
358
|
-
```tsx
|
|
359
|
-
"use client";
|
|
360
|
-
export const Self = () => <Task.View.General task={st.use.task()} />;
|
|
361
|
-
```
|
|
362
|
-
|
|
363
|
-
**⑤ Hand the promise across, not the awaited value.** `ClientInit` / `ClientView` are `PromiseOrObject<T>`, so a
|
|
364
|
-
page may pass an unawaited `fetch.initTaskInTodo()` and `Load.*` resolves it behind a skeleton. `await` blocks the
|
|
365
|
-
shell for data the page needs immediately; passing the promise streams the rest. Independent fetches still go
|
|
366
|
-
through one `Promise.all`.
|
|
367
|
-
|
|
368
|
-
**⑥ Use named `ReactNode` slots, not just `children`.** A client shell can take several server-rendered subtrees:
|
|
369
|
-
`Layout.Navbar` accepts `title`, `back`, `left`, `right`, and `children`, so it composes server content in five
|
|
370
|
-
places instead of absorbing it.
|
|
371
|
-
|
|
372
|
-
**⑦ Let the server do the derived work.** Display and predicate logic belongs on `LightTask` (`isNew()`,
|
|
373
|
-
`canWrite(user?)`, `formatTimes()`), and enum→class lookups belong in a module-scope `as const` map. Both sides call
|
|
374
|
-
the same method, so a client component that exists only to compute a label is markup in the wrong place.
|
|
375
|
-
|
|
376
|
-
**⑧ Gate auth on the server.** Check the session in `_layout.tsx` and redirect there, before any HTML is sent. A
|
|
377
|
-
client-side auth check costs a hydration round-trip and flashes the wrong UI first.
|
|
378
|
-
|
|
379
|
-
**⑨ Prefer CSS over client state for pure visibility.** A `data-*` attribute plus `group-data-[…]` variants, or
|
|
380
|
-
`<details>`/`<summary>`, keeps both branches server-rendered. Reach for `useState` when the state is real, not when
|
|
381
|
-
a Tailwind variant would do.
|
|
382
|
-
|
|
383
|
-
**⑩ Keep the heavy island out of the first load.** A large client-only widget goes behind the
|
|
384
|
-
`ui/<Folder>/index_.tsx` + `lazy()` pair so the server renders the page around it.
|
|
385
265
|
|
|
386
266
|
## Code Style
|
|
387
267
|
|
|
@@ -420,11 +300,14 @@ is convention that keeps hand-written code reading like generated code.
|
|
|
420
300
|
`.Template.tsx` files contain zero `useState`: forms are store-driven with `Field.*`, `value={taskForm.x}`, and
|
|
421
301
|
`onChange={st.do.setXOnTask}` passed by reference.
|
|
422
302
|
- Read with `st.use.*` and write with `st.do.*`. Client components do not call `fetch.*`.
|
|
423
|
-
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
-
|
|
427
|
-
|
|
303
|
+
- Reach for a recipe before writing a look by hand: `buttonRecipe`, `badgeRecipe` and `inputRecipe` from
|
|
304
|
+
`akanjs/ui` carry the button, badge and field surfaces. Call them as `buttonRecipe(variants?, className?)` —
|
|
305
|
+
the second argument merges internally and takes an array, so never wrap it in `cn()`.
|
|
306
|
+
- Static class strings stay plain strings. Reach for `cn` only for a conditional or to merge an incoming
|
|
307
|
+
`className`, and merge the caller last: `cn("base", conditional, className)`. `cn` comes from `akanjs/client`
|
|
308
|
+
and is the only class-combining function — no `clsx`, no raw `twMerge`.
|
|
309
|
+
- Use semantic tokens with opacity modifiers (`text-foreground/60`, `bg-background/70`, `border-border`). Never
|
|
310
|
+
`dark:` — theming is the token block in `page/*/styles.css`, where each token has a `-foreground` pair.
|
|
428
311
|
- Hoist enum→class lookups to a module-scope `as const` map typed `{ [key in cnst.TaskStatus["value"]]: string }`,
|
|
429
312
|
not `Record<...>`.
|
|
430
313
|
|
|
@@ -650,7 +533,7 @@ export class TaskService extends serve(db.task, ({ plug }) => ({
|
|
|
650
533
|
For a custom adapter class (not a predefined role), pass the class itself, e.g. `ipfsApi: plug(IpfsApi)`
|
|
651
534
|
(see `libs/shared/lib/file/file.service.ts`). Injecting a file/image field is usually simpler than calling
|
|
652
535
|
storage directly: declare `image: field(File).optional()` (or `images: field([File])`) on the model and let the
|
|
653
|
-
store's generated `upload<Field>On<Model>(fileList)` action handle the upload. Add `{ cascade: "
|
|
536
|
+
store's generated `upload<Field>On<Model>(fileList)` action handle the upload. Add `{ cascade: "remove" }` to that
|
|
654
537
|
field when the file belongs to the model alone, and removing the model removes the file and its stored object.
|
|
655
538
|
|
|
656
539
|
---
|
|
@@ -868,23 +751,12 @@ A short list of things the type system does not always catch:
|
|
|
868
751
|
`secret` / `hidden` / `resolve()` fields with `text` throw at class-build time — the mirror is plaintext. Search
|
|
869
752
|
runs on sqlite/libsql only; `q.search()` against Postgres throws. `thumb` is mirrored for rendering and is not
|
|
870
753
|
indexed.
|
|
871
|
-
- **`cascade`
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
children exist. The removal runs through the **target's service** so its `_postRemove` runs too, unless the
|
|
878
|
-
target provably has no removal side effect, in which case the boot-time plan collapses it into one query. A
|
|
879
|
-
`removeWith` field gets its index automatically. Nothing checks for other references to the same target, so
|
|
880
|
-
`removeRef` asserts exclusive ownership. Removal is soft but a storage delete is not, and a query-level removal
|
|
881
|
-
fires no hooks and therefore no cascade.
|
|
882
|
-
- **Removal is always soft, and `delete` is reserved.** `remove(id)`, the facade's `removeMany(query)`, and the
|
|
883
|
-
store's `removeManyByQuery` all stamp `removedAt`; the framework has no hard delete for a model table.
|
|
884
|
-
- **The model facade spells out `Many`/`One` on its writes** — `updateOne`, `updateMany`, `removeOne`, `removeMany`
|
|
885
|
-
— because a bare `update`/`remove` would read like the document-path `update(id)` / `doc.remove()` while hitting
|
|
886
|
-
every match. Reads keep the short `find`/`findOne` pair, and counting is `count(query)` (`countDocuments` is
|
|
887
|
-
`@deprecated`).
|
|
754
|
+
- **`cascade: "remove"` takes a relation's target down with its owner.** Declare it on the relation itself —
|
|
755
|
+
`image: field(File, { cascade: "remove" })`, arrays included. The removal runs through the **target's service**,
|
|
756
|
+
so the target's own `_postRemove` runs too; that is how removing a model also deletes the file's stored object.
|
|
757
|
+
Only a relation accepts it: a `String`, an `ID`, or a scalar throws while the class is being built. Nothing
|
|
758
|
+
checks for other references to the same target, so declaring it asserts exclusive ownership. Document removal is
|
|
759
|
+
soft but the storage delete is not, and query-level removal fires no hooks and therefore no cascade.
|
|
888
760
|
- **`q.search()` is a filter node, not a slice requirement.** Prefer
|
|
889
761
|
`bySearch: filter().arg("text", String).query((text, q) => q.search(text, { prefix: true }))` — the generated
|
|
890
762
|
`listBySearch` / `countBySearch` / `queryBySearch` / `insightBySearch` come for free. Only add a search slice when
|
|
@@ -962,12 +834,8 @@ akan sync automatically generates APIs across all layers. Only write custom logi
|
|
|
962
834
|
| `pick[Query](args)`, `pickId[Query](args)` | Find one (throw if not found) |
|
|
963
835
|
| `exists[Query](args)`, `count[Query](args)` | Existence check and count |
|
|
964
836
|
| `insight[Query](args)`, `query[Query](args)` | Insight and raw query |
|
|
965
|
-
| `remove[Query](args)`, `removeOne[Query](args)` | Query-level soft remove — all matches, or the newest one (`createdAt` desc, not caller-chosen) |
|
|
966
|
-
| `update[Query](args).set(patch)`, `updateOne[Query](args).set(patch)` | Query-level update — the patch lands on a terminal `.set()`, because a filter's trailing args may be optional |
|
|
967
|
-
|
|
968
|
-
**Rule**: Define `Filter` with `.query()` conditions in `document.ts`. akan sync auto-generates all 14 query helper methods per filter. Write `Document` chain methods only for state transitions with validation.
|
|
969
837
|
|
|
970
|
-
**
|
|
838
|
+
**Rule**: Define `Filter` with `.query()` conditions in `document.ts`. akan sync auto-generates all 10 query helper methods per filter. Write `Document` chain methods only for state transitions with validation.
|
|
971
839
|
|
|
972
840
|
## Generated Context
|
|
973
841
|
|
|
@@ -210,6 +210,25 @@
|
|
|
210
210
|
"**/*.Zone.tsx"
|
|
211
211
|
],
|
|
212
212
|
"plugins": ["./node_modules/@akanjs/devkit/lint/no-import-external-library.grit"]
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
"includes": [
|
|
216
|
+
"apps/**/*.ts",
|
|
217
|
+
"apps/**/*.tsx",
|
|
218
|
+
"libs/**/*.ts",
|
|
219
|
+
"libs/**/*.tsx",
|
|
220
|
+
"!**/*.test.ts",
|
|
221
|
+
"!**/*.test.tsx",
|
|
222
|
+
"!**/*.spec.ts",
|
|
223
|
+
"!**/*.spec.tsx"
|
|
224
|
+
],
|
|
225
|
+
"plugins": [
|
|
226
|
+
"./node_modules/@akanjs/devkit/lint/no-raw-palette-class.grit",
|
|
227
|
+
"./node_modules/@akanjs/devkit/lint/no-arbitrary-color.grit",
|
|
228
|
+
"./node_modules/@akanjs/devkit/lint/no-daisyui-legacy-class.grit",
|
|
229
|
+
"./node_modules/@akanjs/devkit/lint/no-inline-color.grit",
|
|
230
|
+
"./node_modules/@akanjs/devkit/lint/no-interpolated-arbitrary-class.grit"
|
|
231
|
+
]
|
|
213
232
|
}
|
|
214
233
|
]
|
|
215
234
|
}
|
|
@@ -49,8 +49,11 @@ See `docs/GENERATED.md` for the generated file list.
|
|
|
49
49
|
|
|
50
50
|
`AGENTS.md` holds the full style guide. The rules agents break most often:
|
|
51
51
|
|
|
52
|
-
- **Never hand-order Tailwind classes** — the linter sorts them, including inside `
|
|
52
|
+
- **Never hand-order Tailwind classes** — the linter sorts them, including inside `cn()`, and its output looks
|
|
53
53
|
unnatural on purpose.
|
|
54
|
+
- **Never use a colour outside the semantic vocabulary** — `bg-red-500`, `bg-[#3b82f6]` and `text-base-content`
|
|
55
|
+
compile to no CSS at all, so the element renders unstyled with no error. Use `bg-primary`, `text-foreground/70`
|
|
56
|
+
and the other tokens defined in `page/*/styles.css`.
|
|
54
57
|
- **Never `throw new Error`** — throw `new Err("<module>.error.<key>")` with an `[en, ko]` entry in the module
|
|
55
58
|
dictionary. `common/` cannot import `Err`, so keep throwing code out of it.
|
|
56
59
|
- **Never import a third-party package** from a page, a barrel, or a module file. Re-export it through a one-line
|
|
@@ -1,31 +1,31 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
WorkflowRunner
|
|
4
|
-
} from "./index-
|
|
4
|
+
} from "./index-csqk9z5j.js";
|
|
5
5
|
import {
|
|
6
6
|
PrimitiveScript
|
|
7
|
-
} from "./index-
|
|
7
|
+
} from "./index-r4w8cry5.js";
|
|
8
8
|
import {
|
|
9
9
|
ModuleScript
|
|
10
|
-
} from "./index-
|
|
11
|
-
import"./index-
|
|
10
|
+
} from "./index-vhv3xnjz.js";
|
|
11
|
+
import"./index-hjzr8ja1.js";
|
|
12
12
|
import {
|
|
13
13
|
ScalarScript
|
|
14
|
-
} from "./index-
|
|
14
|
+
} from "./index-fmgbwsr5.js";
|
|
15
15
|
import"./index-ss469dec.js";
|
|
16
|
-
import"./index-
|
|
17
|
-
import"./index-
|
|
16
|
+
import"./index-3pgtw35d.js";
|
|
17
|
+
import"./index-4yp06pn1.js";
|
|
18
18
|
import {
|
|
19
19
|
createWorkflowStepRegistry
|
|
20
|
-
} from "./index-
|
|
21
|
-
import"./index-
|
|
20
|
+
} from "./index-4zgs8g1w.js";
|
|
21
|
+
import"./index-xwgm94zr.js";
|
|
22
22
|
import {
|
|
23
23
|
Workspace,
|
|
24
24
|
command,
|
|
25
25
|
script
|
|
26
|
-
} from "./index-
|
|
27
|
-
import"./index-
|
|
28
|
-
import"./index-
|
|
26
|
+
} from "./index-jk9q6pse.js";
|
|
27
|
+
import"./index-jtm86byd.js";
|
|
28
|
+
import"./index-6pz1j0zj.js";
|
|
29
29
|
import"./index-xys926f2.js";
|
|
30
30
|
import"./index-67546d0j.js";
|
|
31
31
|
import"./index-r24hmh0q.js";
|
|
@@ -1,48 +1,48 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
import {
|
|
3
3
|
ContextScript
|
|
4
|
-
} from "./index-
|
|
5
|
-
import"./index-
|
|
6
|
-
import"./index-
|
|
7
|
-
import"./index-
|
|
8
|
-
import"./index-
|
|
9
|
-
import"./index-
|
|
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
10
|
import {
|
|
11
11
|
CloudScript
|
|
12
|
-
} from "./index-
|
|
12
|
+
} from "./index-ybybkec0.js";
|
|
13
13
|
import {
|
|
14
14
|
getLatestPackageVersion,
|
|
15
15
|
getNpmRegistryUrl
|
|
16
|
-
} from "./index-
|
|
16
|
+
} from "./index-xj8bks3p.js";
|
|
17
17
|
import {
|
|
18
18
|
PackageScript
|
|
19
|
-
} from "./index-
|
|
20
|
-
import"./index-
|
|
19
|
+
} from "./index-e3qskyt2.js";
|
|
20
|
+
import"./index-fmgbwsr5.js";
|
|
21
21
|
import"./index-ss469dec.js";
|
|
22
|
-
import"./index-
|
|
22
|
+
import"./index-y0k42k04.js";
|
|
23
23
|
import {
|
|
24
24
|
GlobalConfig
|
|
25
|
-
} from "./index-
|
|
25
|
+
} from "./index-3pgtw35d.js";
|
|
26
26
|
import {
|
|
27
27
|
AgentScript
|
|
28
|
-
} from "./index-
|
|
29
|
-
import"./index-
|
|
30
|
-
import"./index-
|
|
31
|
-
import"./index-
|
|
28
|
+
} from "./index-n0z3f2xg.js";
|
|
29
|
+
import"./index-4yp06pn1.js";
|
|
30
|
+
import"./index-4zgs8g1w.js";
|
|
31
|
+
import"./index-xwgm94zr.js";
|
|
32
32
|
import {
|
|
33
33
|
ApplicationScript
|
|
34
|
-
} from "./index-
|
|
34
|
+
} from "./index-tq1y56tp.js";
|
|
35
35
|
import"./index-76rn3g2c.js";
|
|
36
36
|
import {
|
|
37
37
|
LibraryScript
|
|
38
|
-
} from "./index-
|
|
38
|
+
} from "./index-cn24dcqn.js";
|
|
39
39
|
import {
|
|
40
40
|
Exec,
|
|
41
41
|
Workspace,
|
|
42
42
|
command,
|
|
43
43
|
runner,
|
|
44
44
|
script
|
|
45
|
-
} from "./index-
|
|
45
|
+
} from "./index-jk9q6pse.js";
|
|
46
46
|
import"./index-fgc8r6dj.js";
|
|
47
47
|
import {
|
|
48
48
|
ThemeValidator,
|
|
@@ -59,8 +59,8 @@ import {
|
|
|
59
59
|
collectScopeRecipeSources,
|
|
60
60
|
extractAgentBlock,
|
|
61
61
|
renderScopeAgentBlock
|
|
62
|
-
} from "./index-
|
|
63
|
-
import"./index-
|
|
62
|
+
} from "./index-jtm86byd.js";
|
|
63
|
+
import"./index-6pz1j0zj.js";
|
|
64
64
|
import {
|
|
65
65
|
collectRecipeSources,
|
|
66
66
|
scanRecipes
|
|
@@ -1,31 +0,0 @@
|
|
|
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
|
-
}
|