@akanjs/cli 3.0.0-alpha.99 → 3.0.0-beta.0

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 (71) hide show
  1. package/.build-stamp +1 -1
  2. package/{abstractCompactor-byytxs22.js → abstractCompactor-mjh5ppsq.js} +1 -1
  3. package/{agent.command-v1a4746v.js → agent.command-d2gfxfck.js} +5 -5
  4. package/{application.command-j05fr5jc.js → application.command-pj2m7584.js} +5 -5
  5. package/{applicationBuildRunner-s4gsk728.js → applicationBuildRunner-kxwbssgn.js} +2 -2
  6. package/{applicationReleasePackager-28v9vbzk.js → applicationReleasePackager-4yasxwv9.js} +1 -1
  7. package/buildBatch.proc.js +2 -2
  8. package/{capacitorApp-8d6d4zkm.js → capacitorApp-36rbr858.js} +2 -2
  9. package/{cloud.command-r6wvq2rp.js → cloud.command-jshwdq22.js} +9 -9
  10. package/{context.command-4vdt1aqq.js → context.command-ez8a0zyz.js} +13 -13
  11. package/{devTui-tnj2hh4f.js → devTui-40ggmd99.js} +85 -84
  12. package/{devTuiView-xzed2pd6.js → devTuiView-c08ccbat.js} +1 -1
  13. package/{guideline.command-p92dq6ts.js → guideline.command-xmqwx8zm.js} +11 -7
  14. package/guidelines/agentRule/agentRule.instruction.md +92 -28
  15. package/guidelines/conventions/conventions.instruction.md +97 -39
  16. package/guidelines/mcpRule/mcpRule.generate.json +11 -1
  17. package/guidelines/mcpRule/mcpRule.instruction.md +296 -53
  18. package/guidelines/queryRule/queryRule.instruction.md +17 -9
  19. package/guidelines/ssrRule/ssrRule.instruction.md +15 -13
  20. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +10 -5
  21. package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +68 -3
  22. package/incrementalBuilder.proc.js +1 -1
  23. package/{index-gq44bqjt.js → index-259dsnz3.js} +2 -2
  24. package/{index-b9rkcrx8.js → index-2hq52jq3.js} +9 -7
  25. package/{index-qsq3q8fy.js → index-2tv3fdx4.js} +21 -10
  26. package/{index-qkk27bk6.js → index-4edzf1gr.js} +4 -4
  27. package/{index-yza7zkpt.js → index-600zhnve.js} +10 -10
  28. package/{index-srdssqj7.js → index-7737gxcp.js} +3 -3
  29. package/{index-zmjppp25.js → index-8pm30m73.js} +5 -5
  30. package/{index-p5neav9b.js → index-a64esmtb.js} +54 -9
  31. package/{index-0amdxycf.js → index-fz5q3gd7.js} +3 -3
  32. package/{index-fc1bh0a7.js → index-gyd220p2.js} +1 -1
  33. package/{index-5svjba73.js → index-j9yhg404.js} +1 -1
  34. package/{index-c69j78kk.js → index-kn1h7p1x.js} +1 -1
  35. package/{index-gtmz74tj.js → index-ktb5njgy.js} +2 -2
  36. package/{index-znynzt5w.js → index-mak3cd28.js} +1 -1
  37. package/{index-29sxqfaw.js → index-ne2qy0nk.js} +3 -3
  38. package/{index-hyywke0f.js → index-pnqbkf5p.js} +1 -1
  39. package/{index-8p8p0cf8.js → index-rwg2aa4f.js} +3 -3
  40. package/{index-5wd728df.js → index-v0qh106m.js} +215 -12
  41. package/{index-7d6vny25.js → index-ws5e0j51.js} +2 -2
  42. package/{index-1yxtbmbj.js → index-wt7qrtd4.js} +1 -1
  43. package/{index-gkkdx9kp.js → index-zry5nz81.js} +6 -6
  44. package/index.js +19 -19
  45. package/{library.command-s4fszngs.js → library.command-nbsky672.js} +3 -3
  46. package/{localRegistry.command-fq2w7cgz.js → localRegistry.command-yeq57wjr.js} +8 -8
  47. package/{module.command-6n7dwvav.js → module.command-29vqzrh8.js} +6 -6
  48. package/{package.command-gr82ypnr.js → package.command-axv0abmn.js} +3 -3
  49. package/package.json +2 -2
  50. package/{page.command-7gvgb3jb.js → page.command-vs9s3zrh.js} +3 -3
  51. package/{primitive.command-tk5ckw38.js → primitive.command-cbxmqbtc.js} +7 -7
  52. package/{quality.command-38v81baf.js → quality.command-gmbs7nws.js} +2 -2
  53. package/{repair.command-5xbwvsxp.js → repair.command-bqsjptsj.js} +5 -5
  54. package/{routeSourceValidator-ze6x894c.js → routeSourceValidator-apyqw7wk.js} +90 -17
  55. package/{scalar.command-0epwcr5k.js → scalar.command-6k1vhe9s.js} +5 -5
  56. package/{subspace.command-e2t36xb2.js → subspace.command-fwjfcts7.js} +7 -5
  57. package/templates/app/page/_index.tsx +3 -3
  58. package/templates/app/page/_layout.tsx +31 -32
  59. package/templates/appSample/page/task/[taskId]/_index.tsx +22 -27
  60. package/templates/appSample/page/task/[taskId]/edit.tsx +25 -30
  61. package/templates/appSample/page/task/_index.tsx +3 -7
  62. package/templates/appSample/page/task/_layout.tsx +4 -15
  63. package/templates/appSample/page/task/new.tsx +4 -10
  64. package/templates/crudPages/[__model__Id]/edit/page.tsx +26 -28
  65. package/templates/crudPages/[__model__Id]/page.tsx +20 -25
  66. package/templates/crudPages/new/page.tsx +24 -23
  67. package/templates/crudPages/page.tsx +25 -24
  68. package/templates/crudSinglePage/page.tsx +25 -24
  69. package/templates/lib/sig.ts +4 -0
  70. package/{workflow.command-kphbr7eh.js → workflow.command-nwpcdc8p.js} +10 -10
  71. package/{workspace.command-ag5hy887.js → workspace.command-8s050m0h.js} +20 -20
@@ -2,6 +2,7 @@
2
2
  // pkgs/@akanjs/devkit/routeSourceValidator.ts
3
3
  import { getRouteExports } from "akanjs/common";
4
4
  import ts from "typescript";
5
+ var chainRoots = new Set(["page", "layout", "rootLayout"]);
5
6
 
6
7
  class RouteSourceValidator {
7
8
  static validateRouteSourceExports(source, filePath, kind, options = {}) {
@@ -62,7 +63,74 @@ class RouteSourceValidator {
62
63
  if (exported.has("metadata") && exported.has("generateMetadata")) {
63
64
  throw new Error(`[route-convention] metadata and generateMetadata cannot both be exported in ${filePath}`);
64
65
  }
65
- return { devOnly: RouteSourceValidator.#readDevOnly(sourceFile, filePath) };
66
+ const chain = RouteSourceValidator.#readChain(sourceFile, filePath);
67
+ if (!chain)
68
+ return { devOnly: RouteSourceValidator.#readDevOnly(sourceFile, filePath) };
69
+ const named = [...exported].filter((name) => name !== "default");
70
+ if (named.length)
71
+ throw new Error(`[route-convention] ${filePath} exports ${named.join(", ")} beside its ${chain.kind}() chain \u2014 every route setting is a stage of the chain`);
72
+ if (kind === "page" && chain.kind !== "page")
73
+ throw new Error(`[route-convention] ${filePath} is a page file but exports ${chain.kind}()`);
74
+ if (kind === "layout" && chain.kind === "page")
75
+ throw new Error(`[route-convention] ${filePath} is a layout file but exports page()`);
76
+ if (options.pattern)
77
+ RouteSourceValidator.#assertChainParams(chain, options.pattern, filePath);
78
+ return {
79
+ devOnly: chain.devOnly,
80
+ chain: { kind: chain.kind, params: chain.params, ...chain.prompt ? { prompt: chain.prompt } : {} }
81
+ };
82
+ }
83
+ static #readChain(sourceFile, filePath) {
84
+ for (const statement of sourceFile.statements) {
85
+ if (!ts.isExportAssignment(statement) || statement.isExportEquals)
86
+ continue;
87
+ let node = RouteSourceValidator.#unwrapExpression(statement.expression);
88
+ const stages = [];
89
+ while (node && ts.isCallExpression(node)) {
90
+ const callee = node.expression;
91
+ if (ts.isPropertyAccessExpression(callee)) {
92
+ stages.unshift({ name: callee.name.text, args: node.arguments });
93
+ node = callee.expression;
94
+ continue;
95
+ }
96
+ if (ts.isIdentifier(callee) && chainRoots.has(callee.text))
97
+ return RouteSourceValidator.#chainOf(callee.text, stages, filePath);
98
+ return null;
99
+ }
100
+ return null;
101
+ }
102
+ return null;
103
+ }
104
+ static #chainOf(kind, stages, filePath) {
105
+ const params = [];
106
+ let prompt;
107
+ let devOnly = false;
108
+ const literal = (stage) => {
109
+ const first = stage.args[0];
110
+ if (!first || !ts.isStringLiteralLike(first))
111
+ throw new Error(`[route-convention] .${stage.name}() takes a string literal first in ${filePath} \u2014 the build reads it without evaluating the module`);
112
+ return first.text;
113
+ };
114
+ for (const stage of stages) {
115
+ if (stage.name === "param")
116
+ params.push(literal(stage));
117
+ else if (stage.name === "prompt")
118
+ prompt = literal(stage);
119
+ else if (stage.name === "config")
120
+ devOnly = RouteSourceValidator.#devOnlyOf(RouteSourceValidator.#unwrapExpression(stage.args[0]), filePath);
121
+ }
122
+ return { kind, params, prompt, devOnly };
123
+ }
124
+ static #assertChainParams(chain, pattern, filePath) {
125
+ const inPath = pattern.split("/").filter((part) => part.startsWith(":")).map((part) => part.slice(1)).filter((name) => name !== "lang");
126
+ if (chain.params.includes("lang"))
127
+ throw new Error(`[route-convention] ${filePath} declares .param("lang"), which every route receives undeclared`);
128
+ const unknown = chain.params.find((name) => !inPath.includes(name));
129
+ if (unknown)
130
+ throw new Error(`[route-convention] ${filePath} declares .param("${unknown}") but no [${unknown}] segment is in its path`);
131
+ const undeclared = inPath.find((name) => !chain.params.includes(name));
132
+ if (chain.kind === "page" && undeclared)
133
+ throw new Error(`[route-convention] ${filePath} sits under [${undeclared}] but declares no .param("${undeclared}") \u2014 a page reads only what it declares`);
66
134
  }
67
135
  static #readDevOnly(sourceFile, filePath) {
68
136
  for (const statement of sourceFile.statements) {
@@ -75,26 +143,31 @@ class RouteSourceValidator {
75
143
  if (!ts.isIdentifier(declaration.name) || declaration.name.text !== "pageConfig")
76
144
  continue;
77
145
  const initializer = RouteSourceValidator.#unwrapExpression(declaration.initializer);
78
- if (!initializer || !ts.isObjectLiteralExpression(initializer))
79
- continue;
80
- for (const property of initializer.properties) {
81
- if (!ts.isPropertyAssignment(property))
82
- continue;
83
- const name = property.name;
84
- const key = ts.isIdentifier(name) ? name.text : ts.isStringLiteral(name) ? name.text : null;
85
- if (key !== "devOnly")
86
- continue;
87
- const value = RouteSourceValidator.#unwrapExpression(property.initializer);
88
- if (value?.kind === ts.SyntaxKind.TrueKeyword)
89
- return true;
90
- if (value?.kind === ts.SyntaxKind.FalseKeyword)
91
- return false;
92
- throw new Error(`[route-convention] pageConfig.devOnly must be a literal true or false in ${filePath} \u2014 the build reads it without evaluating the module`);
93
- }
146
+ if (initializer && ts.isObjectLiteralExpression(initializer))
147
+ return RouteSourceValidator.#devOnlyOf(initializer, filePath);
94
148
  }
95
149
  }
96
150
  return false;
97
151
  }
152
+ static #devOnlyOf(config, filePath) {
153
+ if (!config || !ts.isObjectLiteralExpression(config))
154
+ return false;
155
+ for (const property of config.properties) {
156
+ if (!ts.isPropertyAssignment(property))
157
+ continue;
158
+ const name = property.name;
159
+ const key = ts.isIdentifier(name) ? name.text : ts.isStringLiteral(name) ? name.text : null;
160
+ if (key !== "devOnly")
161
+ continue;
162
+ const value = RouteSourceValidator.#unwrapExpression(property.initializer);
163
+ if (value?.kind === ts.SyntaxKind.TrueKeyword)
164
+ return true;
165
+ if (value?.kind === ts.SyntaxKind.FalseKeyword)
166
+ return false;
167
+ throw new Error(`[route-convention] devOnly must be a literal true or false in ${filePath} \u2014 the build reads it without evaluating the module`);
168
+ }
169
+ return false;
170
+ }
98
171
  static #unwrapExpression(expression) {
99
172
  let current = expression;
100
173
  while (current && (ts.isAsExpression(current) || ts.isSatisfiesExpression(current) || ts.isParenthesizedExpression(current))) {
@@ -2,18 +2,18 @@
2
2
  import {
3
3
  Sys,
4
4
  command
5
- } from "./index-znynzt5w.js";
5
+ } from "./index-mak3cd28.js";
6
6
  import"./index-q0y2jq9j.js";
7
- import"./index-qsq3q8fy.js";
7
+ import"./index-2tv3fdx4.js";
8
8
  import"./index-6wwzs1bc.js";
9
9
  import {
10
10
  renderPrimitiveReport
11
- } from "./index-fc1bh0a7.js";
12
- import"./index-c69j78kk.js";
11
+ } from "./index-gyd220p2.js";
12
+ import"./index-kn1h7p1x.js";
13
13
  import"./index-h7yw2z9n.js";
14
14
  import {
15
15
  ScalarScript
16
- } from "./index-8p8p0cf8.js";
16
+ } from "./index-rwg2aa4f.js";
17
17
 
18
18
  // pkgs/@akanjs/cli/scalar/scalar.command.ts
19
19
  import { Logger, lowerlize } from "akanjs/common";
@@ -4,7 +4,7 @@ import {
4
4
  runner,
5
5
  script,
6
6
  command
7
- } from "./index-znynzt5w.js";
7
+ } from "./index-mak3cd28.js";
8
8
  import {
9
9
  FileSys
10
10
  } from "./index-q0y2jq9j.js";
@@ -12,11 +12,11 @@ import {
12
12
  Executor,
13
13
  WorkspaceExecutor,
14
14
  AppExecutor
15
- } from "./index-qsq3q8fy.js";
15
+ } from "./index-2tv3fdx4.js";
16
16
  import"./index-6wwzs1bc.js";
17
17
  import {
18
18
  SlicePlanner
19
- } from "./index-5svjba73.js";
19
+ } from "./index-j9yhg404.js";
20
20
  import {
21
21
  LibSource
22
22
  } from "./index-3d9y066c.js";
@@ -147,7 +147,7 @@ class Subspace {
147
147
  }
148
148
  const clone = new Executor(`subspace-${this.name}`, this.#clonePath);
149
149
  await clone.spawn("git", ["fetch", "--quiet", "origin", branch]);
150
- await clone.spawn("git", ["checkout", "--quiet", "-B", branch, `origin/${branch}`]);
150
+ await clone.spawn("git", ["checkout", "--quiet", "-f", "-B", branch, `origin/${branch}`]);
151
151
  await clone.spawn("git", ["clean", "-qfd"]);
152
152
  return clone;
153
153
  }
@@ -324,7 +324,9 @@ class Subspace {
324
324
  "--",
325
325
  `libs/${lib}`
326
326
  ]);
327
- const files = listed.split("\x00").filter((file) => !!file && !this.#isSubspaceOwned(file)).sort();
327
+ const candidates = listed.split("\x00").filter((file) => !!file && !this.#isSubspaceOwned(file)).sort();
328
+ const present = await Promise.all(candidates.map(async (file) => await FileSys.entryExists(path.join(this.#clonePath, file))));
329
+ const files = candidates.filter((_, index) => present[index]);
328
330
  const hasher = new Bun.CryptoHasher("sha256");
329
331
  for (const file of files) {
330
332
  const content = await FileSys.readText(path.join(this.#clonePath, file));
@@ -8,7 +8,7 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
8
8
  filename: "_index.tsx",
9
9
  content: `
10
10
  import { getEnv } from "akanjs/base";
11
- import { usePage } from "akanjs/client";
11
+ import { page, usePage } from "akanjs/client";
12
12
  import { Link, System } from "akanjs/ui";
13
13
  import {
14
14
  FaBookOpen,
@@ -22,7 +22,7 @@ import {
22
22
  FaTerminal,
23
23
  } from "react-icons/fa";
24
24
 
25
- export default function Page() {
25
+ export default page().render(() => {
26
26
  const appName = getEnv().appName;
27
27
  const { l } = usePage();
28
28
  return (
@@ -294,6 +294,6 @@ export default function Page() {
294
294
  </section>
295
295
  </main>
296
296
  );
297
- }`,
297
+ });`,
298
298
  };
299
299
  }
@@ -9,41 +9,40 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
9
9
  filename: "_layout.tsx",
10
10
  content: `
11
11
  import "./styles.css";
12
- import type { Font, LayoutProps } from "akanjs/client";
12
+ import { rootLayout } from "akanjs/client";
13
13
  ${isUsingShared ? "import { Auth } from '@shared/ui';" : ""}
14
14
 
15
- export const fonts: Font[] = [
16
- {
17
- name: "pretendard",
18
- default: true,
19
- paths: [
20
- { src: "/fonts/Pretendard-Thin.woff2", weight: 100 },
21
- { src: "/fonts/Pretendard-ExtraLight.woff2", weight: 200 },
22
- { src: "/fonts/Pretendard-Light.woff2", weight: 300 },
23
- { src: "/fonts/Pretendard-Regular.woff2", weight: 400 },
24
- { src: "/fonts/Pretendard-Medium.woff2", weight: 500 },
25
- { src: "/fonts/Pretendard-SemiBold.woff2", weight: 600 },
26
- { src: "/fonts/Pretendard-Bold.woff2", weight: 700 },
27
- { src: "/fonts/Pretendard-ExtraBold.woff2", weight: 800 },
28
- { src: "/fonts/Pretendard-Black.woff2", weight: 900 },
29
- ],
30
- },
31
- ];
32
-
33
- export const head = (
34
- <>
35
- <title>${dict.appName}</title>
36
- <link rel="icon" href="/favicon.ico" />
37
- </>
38
- );
39
-
40
- export default function Layout({ children }: LayoutProps) {
41
- return (
15
+ export default rootLayout()
16
+ .fonts([
17
+ {
18
+ name: "pretendard",
19
+ default: true,
20
+ paths: [
21
+ { src: "/fonts/Pretendard-Thin.woff2", weight: 100 },
22
+ { src: "/fonts/Pretendard-ExtraLight.woff2", weight: 200 },
23
+ { src: "/fonts/Pretendard-Light.woff2", weight: 300 },
24
+ { src: "/fonts/Pretendard-Regular.woff2", weight: 400 },
25
+ { src: "/fonts/Pretendard-Medium.woff2", weight: 500 },
26
+ { src: "/fonts/Pretendard-SemiBold.woff2", weight: 600 },
27
+ { src: "/fonts/Pretendard-Bold.woff2", weight: 700 },
28
+ { src: "/fonts/Pretendard-ExtraBold.woff2", weight: 800 },
29
+ { src: "/fonts/Pretendard-Black.woff2", weight: 900 },
30
+ ],
31
+ },
32
+ ])
33
+ .head(
42
34
  <>
43
- {children}${isUsingShared ? "\n <Auth.User />\n <Auth.Admin />" : ""}
44
- </>
45
- );
46
- }
35
+ <title>${dict.appName}</title>
36
+ <link rel="icon" href="/favicon.ico" />
37
+ </>,
38
+ )
39
+ .render(({ children }) => {
40
+ return (
41
+ <>
42
+ {children}${isUsingShared ? "\n <Auth.User />\n <Auth.Admin />" : ""}
43
+ </>
44
+ );
45
+ });
47
46
  `,
48
47
  };
49
48
  }
@@ -3,37 +3,32 @@ import type { AppInfo, LibInfo } from "akanjs";
3
3
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
4
  return {
5
5
  filename: "_index.tsx",
6
- content: `
7
- import { fetch, usePage } from "@apps/${dict.appName}/client";
6
+ content: `import { fetch, usePage } from "@apps/${dict.appName}/client";
8
7
  import { Task } from "@apps/${dict.appName}/lib/task";
8
+ import { ID } from "akanjs/base";
9
+ import { page } from "akanjs/client";
9
10
  import { Link, buttonRecipe } from "akanjs/ui";
10
11
 
11
- // ===== page/task/[taskId]/_index.tsx =====
12
- // Convention: Akan.js file-based routing with dynamic segments.
13
- // [taskId] folder = URL path parameter extracted via useParams() from akanjs/client.
14
- // Uses usePage().l() for i18n — the framework convention for dictionary-based translations.
12
+ export default page()
13
+ .param("taskId", ID)
14
+ .render(async ({ taskId }) => {
15
+ const { l } = usePage();
16
+ const { taskView } = await fetch.viewTask(taskId);
17
+ return (
18
+ <main className="mx-auto max-w-4xl px-6 py-8">
19
+ <div className="mb-6 flex items-center justify-between">
20
+ <a href="/task" className={buttonRecipe({ variant: "ghost", size: "sm" })}>
21
+ {l("task.taskBackToTasks")}
22
+ </a>
23
+ <Link href={\`/task/\${taskId}/edit\`} className={buttonRecipe({ variant: "primary", size: "sm" })}>
24
+ {l("task.taskEdit")}
25
+ </Link>
26
+ </div>
15
27
 
16
- interface PageProps {
17
- params: { taskId: string };
18
- }
19
- export default async function Page({ params: { taskId } }: PageProps) {
20
- const { l } = usePage();
21
- const { taskView } = await fetch.viewTask(taskId);
22
- return (
23
- <main className="mx-auto max-w-4xl px-6 py-8">
24
- <div className="mb-6 flex items-center justify-between">
25
- <a href="/task" className={buttonRecipe({ variant: "ghost", size: "sm" })}>
26
- {l("task.taskBackToTasks")}
27
- </a>
28
- <Link href={\`/task/\${taskId}/edit\`} className={buttonRecipe({ variant: "primary", size: "sm" })}>
29
- {l("task.taskEdit")}
30
- </Link>
31
- </div>
32
-
33
- <Task.Zone.View className="max-w-2xl" view={taskView} />
34
- </main>
35
- );
36
- }
28
+ <Task.Zone.View className="max-w-2xl" view={taskView} />
29
+ </main>
30
+ );
31
+ });
37
32
  `,
38
33
  };
39
34
  }
@@ -4,39 +4,34 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { a
4
4
  return {
5
5
  filename: "edit.tsx",
6
6
  content: `import { fetch, Task } from "@apps/${dict.appName}/client";
7
+ import { ID } from "akanjs/base";
8
+ import { page } from "akanjs/client";
7
9
  import { Load, Link, buttonRecipe } from "akanjs/ui";
8
10
 
9
- // ===== page/task/[taskId]/edit.tsx =====
10
- // Convention: Server-side edit form page using Load.Edit from akanjs/ui.
11
- // async Page() — fetches model data on the server via fetch.editTask(params.taskId).
12
- // The same Template (Task.Template.General) is reused here — no separate edit template needed.
13
- // Load.Edit with type="form" provides the submit/cancel wrapper with the pre-loaded model.
11
+ export default page()
12
+ .param("taskId", ID)
13
+ .render(async ({ taskId }) => {
14
+ const { taskEdit } = await fetch.editTask(taskId);
14
15
 
15
- interface PageProps {
16
- params: { taskId: string };
17
- }
18
- export default async function Page({ params: { taskId } }: PageProps) {
19
- const { taskEdit } = await fetch.editTask(taskId);
20
-
21
- return (
22
- <main className="mx-auto max-w-2xl px-6 py-8">
23
- <div className="mb-6">
24
- <Link href={\`/task/\${taskId}\`} className={buttonRecipe({ variant: "ghost", size: "sm" })}>
25
- ← Back to Task
26
- </Link>
27
- </div>
28
- <div className="mb-6">
29
- <h1 className="font-extrabold text-3xl text-foreground">Edit Task</h1>
30
- <p className="mt-1 text-foreground/60 text-sm">Update the task details</p>
31
- </div>
32
- <div className="rounded-xl border border-foreground/10 bg-background p-6 shadow-sm">
33
- <Load.Edit slice={fetch.slice.taskInPublic} edit={taskEdit} type="form" onSubmit={\`/task/\${taskId}\`}>
34
- <Task.Template.General />
35
- </Load.Edit>
36
- </div>
37
- </main>
38
- );
39
- }
16
+ return (
17
+ <main className="mx-auto max-w-2xl px-6 py-8">
18
+ <div className="mb-6">
19
+ <Link href={\`/task/\${taskId}\`} className={buttonRecipe({ variant: "ghost", size: "sm" })}>
20
+ ← Back to Task
21
+ </Link>
22
+ </div>
23
+ <div className="mb-6">
24
+ <h1 className="font-extrabold text-3xl text-foreground">Edit Task</h1>
25
+ <p className="mt-1 text-foreground/60 text-sm">Update the task details</p>
26
+ </div>
27
+ <div className="rounded-xl border border-foreground/10 bg-background p-6 shadow-sm">
28
+ <Load.Edit slice={fetch.slice.taskInPublic} edit={taskEdit} type="form" onSubmit={\`/task/\${taskId}\`}>
29
+ <Task.Template.General />
30
+ </Load.Edit>
31
+ </div>
32
+ </main>
33
+ );
34
+ });
40
35
  `,
41
36
  };
42
37
  }
@@ -4,14 +4,10 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { a
4
4
  return {
5
5
  filename: "_index.tsx",
6
6
  content: `import { fetch, Task, usePage } from "@apps/${dict.appName}/client";
7
+ import { page } from "akanjs/client";
7
8
  import { Link, buttonRecipe } from "akanjs/ui";
8
9
 
9
- // ===== page/task/_index.tsx =====
10
- // Convention: Akan.js file-based routing — _index.tsx is the index page for /task.
11
- // Server-side data loading via loader() at the page level; passes init/view props to Zone components.
12
- // Uses usePage().l() for i18n — the framework convention for dictionary-based translations.
13
-
14
- export default async function Page() {
10
+ export default page().render(async () => {
15
11
  const { l } = usePage();
16
12
  const { taskInitInPublic } = await fetch.initTaskInPublic();
17
13
  return (
@@ -29,6 +25,6 @@ export default async function Page() {
29
25
  <Task.Zone.Card className="flex flex-col gap-3" init={taskInitInPublic} />
30
26
  </main>
31
27
  );
32
- }`,
28
+ });`,
33
29
  };
34
30
  }
@@ -3,22 +3,11 @@ import type { AppInfo, LibInfo } from "akanjs";
3
3
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
4
  return {
5
5
  filename: "_layout.tsx",
6
- content: `
7
- // ===== page/task/_layout.tsx =====
8
- // Convention: Akan.js file-based routing — _layout.tsx wraps every sub-page under /task/*.
9
- // The underscores mark it as a routing file; children = sub-page component from nested routes.
10
- // Rendered by Akan's router: URL → _layout.tsx → children (e.g., _index.tsx or [taskId]/_index.tsx).
6
+ content: `import { layout } from "akanjs/client";
11
7
 
12
- interface LayoutProps {
13
- children: React.ReactNode;
14
- }
15
- export default function Layout({ children }: LayoutProps) {
16
- return (
17
- <div className="min-h-screen bg-muted">
18
- {children}
19
- </div>
20
- );
21
- }
8
+ export default layout().render(({ children }) => {
9
+ return <div className="min-h-screen bg-muted">{children}</div>;
10
+ });
22
11
  `,
23
12
  };
24
13
  }
@@ -3,17 +3,11 @@ import type { AppInfo, LibInfo } from "akanjs";
3
3
  export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: { appName: string }) {
4
4
  return {
5
5
  filename: "new.tsx",
6
- content: `import { type cnst, fetch, Task, usePage } from "@apps/${dict.appName}/client";
6
+ content: `import { type cnst, fetch, Task } from "@apps/${dict.appName}/client";
7
+ import { page } from "akanjs/client";
7
8
  import { Load, Link, buttonRecipe } from "akanjs/ui";
8
9
 
9
- // ===== page/task/new.tsx =====
10
- // Convention: Server-side form page using Load.Edit from akanjs/ui.
11
- // async Page() — server-side component that pre-initializes form data before rendering.
12
- // Load.Edit with type="form" renders the Template inside a form wrapper with submit/cancel actions.
13
- // onCancel="back" navigates to the previous page; onSubmit specifies the redirect after success.
14
- // Template is reused — same Task.Template.General used for create, edit, and client-side modals.
15
-
16
- export default async function Page() {
10
+ export default page().render(() => {
17
11
  const taskForm: Partial<cnst.Task> = { status: "todo" };
18
12
 
19
13
  return (
@@ -34,6 +28,6 @@ export default async function Page() {
34
28
  </div>
35
29
  </main>
36
30
  );
37
- }`,
31
+ });`,
38
32
  };
39
33
  }
@@ -10,38 +10,36 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
10
10
  filename: "_index.tsx",
11
11
  content: `
12
12
  import { ${dict.Model}, fetch, usePage } from "@apps/${dict.appName}/client";
13
- import type { PageConfig } from "akanjs/client";
13
+ import { ID } from "akanjs/base";
14
+ import { page } from "akanjs/client";
14
15
  import { Load } from "akanjs/ui";
15
16
 
16
- interface PageProps {
17
- params: { ${dict.model}Id: string };
18
- }
19
-
20
- export default async function Page({ params }: PageProps) {
21
- const { l } = usePage();
22
- const { ${dict.model}Id } = params;
23
- const [{ ${dict.model}Edit }] = await Promise.all([fetch.edit${dict.Model}(${dict.model}Id)]);
24
- return (
25
- <div className="container">
26
- <div className="m-4 mt-8 flex justify-between">
27
- <div className="flex items-center gap-2 text-primary text-xl">
28
- {l("base.updateModel", { model: l("${dict.model}.modelName") })}
17
+ export default page()
18
+ .param("${dict.model}Id", ID)
19
+ .config({ transition: "none" })
20
+ .render(async ({ ${dict.model}Id }) => {
21
+ const { l } = usePage();
22
+ const [{ ${dict.model}Edit }] = await Promise.all([fetch.edit${dict.Model}(${dict.model}Id)]);
23
+ return (
24
+ <div className="container">
25
+ <div className="m-4 mt-8 flex justify-between">
26
+ <div className="flex items-center gap-2 text-primary text-xl">
27
+ {l("base.updateModel", { model: l("${dict.model}.modelName") })}
28
+ </div>
29
29
  </div>
30
+ <Load.Edit
31
+ className="flex flex-col items-center"
32
+ slice={fetch.slice.${dict.model}InPublic}
33
+ edit={${dict.model}Edit}
34
+ type="form"
35
+ onCancel="back"
36
+ onSubmit="/${dict.model}"
37
+ >
38
+ <${dict.Model}.Template.General />
39
+ </Load.Edit>
30
40
  </div>
31
- <Load.Edit
32
- className="flex flex-col items-center"
33
- slice={fetch.slice.${dict.model}InPublic}
34
- edit={${dict.model}Edit}
35
- type="form"
36
- onCancel="back"
37
- onSubmit="/${dict.model}"
38
- >
39
- <${dict.Model}.Template.General />
40
- </Load.Edit>
41
- </div>
42
- );
43
- }
44
- export const pageConfig = { transition: "none" } satisfies PageConfig;
41
+ );
42
+ });
45
43
  `,
46
44
  };
47
45
  }
@@ -10,40 +10,35 @@ export default function getContent(scanInfo: AppInfo | LibInfo | null, dict: Dic
10
10
  filename: "_index.tsx",
11
11
  content: `
12
12
  import { ${dict.Model}, fetch, usePage } from "@apps/${dict.appName}/client";
13
- import type { PageConfig } from "akanjs/client";
13
+ import { ID } from "akanjs/base";
14
+ import { page } from "akanjs/client";
14
15
  import { buttonRecipe, Link } from "akanjs/ui";
15
16
 
16
- interface PageProps {
17
- params: { ${dict.model}Id: string };
18
- }
19
-
20
- export function generateHead({ params }: PageProps) {
21
- const { ${dict.model}Id } = params;
22
- return (
17
+ export default page()
18
+ .param("${dict.model}Id", ID)
19
+ .config({ transition: "none" })
20
+ .head(({ ${dict.model}Id }) => (
23
21
  <>
24
22
  <title>{${dict.model}Id}</title>
25
23
  <meta name="description" content={${dict.model}Id} />
26
24
  <meta property="og:title" content={${dict.model}Id} />
27
25
  <meta property="og:description" content={${dict.model}Id} />
28
26
  </>
29
- );
30
- }
31
- export default async function Page({ params }: PageProps) {
32
- const { l } = usePage();
33
- const { ${dict.model}Id } = params;
34
- const [{ ${dict.model}, ${dict.model}View }] = await Promise.all([fetch.view${dict.Model}(${dict.model}Id)]);
35
- return (
36
- <div className="container flex flex-col gap-4">
37
- <div className="flex items-center gap-4 font-bold text-lg">
38
- <${dict.Model}.Zone.View view={${dict.model}View} />
39
- <Link className={buttonRecipe()} href={\`/${dict.model}/\${${dict.model}.id}/edit\`}>
40
- {l("base.updateModel", { model: l("${dict.model}.modelName") })}
41
- </Link>
27
+ ))
28
+ .render(async ({ ${dict.model}Id }) => {
29
+ const { l } = usePage();
30
+ const [{ ${dict.model}, ${dict.model}View }] = await Promise.all([fetch.view${dict.Model}(${dict.model}Id)]);
31
+ return (
32
+ <div className="container flex flex-col gap-4">
33
+ <div className="flex items-center gap-4 font-bold text-lg">
34
+ <${dict.Model}.Zone.View view={${dict.model}View} />
35
+ <Link className={buttonRecipe()} href={\`/${dict.model}/\${${dict.model}.id}/edit\`}>
36
+ {l("base.updateModel", { model: l("${dict.model}.modelName") })}
37
+ </Link>
38
+ </div>
42
39
  </div>
43
- </div>
44
- );
45
- }
46
- export const pageConfig = { transition: "none" } satisfies PageConfig;
40
+ );
41
+ });
47
42
  `,
48
43
  };
49
44
  }