@akanjs/cli 3.0.0-alpha.0 → 3.0.0-alpha.10

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 (89) 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-f0gqkfgz.js} +8 -7
  4. package/{application.command-3smsv7mq.js → application.command-x903m2qq.js} +6 -5
  5. package/{applicationBuildRunner-aq7mb3z6.js → applicationBuildRunner-ha3y8ew8.js} +4 -4
  6. package/{applicationReleasePackager-yrrhbkv3.js → applicationReleasePackager-gg8ccbeh.js} +1 -1
  7. package/buildBatch.proc.js +5 -4
  8. package/{capacitorApp-q3mpmkkz.js → capacitorApp-9xswcgx7.js} +6 -3
  9. package/{cloud.command-rk4e9vrt.js → cloud.command-zs6kyf83.js} +10 -9
  10. package/{context.command-ygzyb0xb.js → context.command-fmepgjzg.js} +16 -15
  11. package/getDirname-m9s0cmhq.js +8 -0
  12. package/{guideline.command-1m1ctar3.js → guideline.command-n6sw0a0c.js} +6 -5
  13. package/guidelines/conventions/conventions.generate.json +25 -0
  14. package/guidelines/conventions/conventions.instruction.md +668 -0
  15. package/guidelines/ssrRule/ssrRule.generate.json +31 -0
  16. package/guidelines/ssrRule/ssrRule.instruction.md +108 -0
  17. package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +24 -0
  18. package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +972 -0
  19. package/incrementalBuilder.proc.js +4 -3
  20. package/{index-3pgtw35d.js → index-0cj2zxbm.js} +1 -1
  21. package/index-1577bej2.js +7 -0
  22. package/{index-r4w8cry5.js → index-3vta1tqa.js} +4 -4
  23. package/{index-ktprgc5g.js → index-4m0bphac.js} +4 -4
  24. package/{index-h0bngh79.js → index-5sn5w2mt.js} +13 -13
  25. package/{index-ev6rjw9n.js → index-62955kn5.js} +37 -14
  26. package/{index-4yp06pn1.js → index-774gcx29.js} +49 -32
  27. package/{index-sggm9qkv.js → index-779ske1y.js} +86 -17
  28. package/{index-f4gqk0dd.js → index-82jrh4hj.js} +6 -6
  29. package/{index-cn24dcqn.js → index-d43t8af2.js} +2 -2
  30. package/{index-n0z3f2xg.js → index-eyyf2149.js} +34 -6
  31. package/{index-e3qskyt2.js → index-gcv8ncfh.js} +2 -2
  32. package/{index-xwgm94zr.js → index-j37qq1f2.js} +1 -1
  33. package/{index-rh16j3c7.js → index-j9cxndc0.js} +3 -2
  34. package/{index-b978vs2e.js → index-jf2ya7r2.js} +3 -3
  35. package/{index-vhv3xnjz.js → index-jhpa13zj.js} +8 -8
  36. package/{index-jk9q6pse.js → index-kdh76j26.js} +4 -2
  37. package/{index-6pz1j0zj.js → index-mxvakhsm.js} +6 -2
  38. package/{index-pyt2b4nw.js → index-qb61h0k7.js} +52 -17
  39. package/{index-csqk9z5j.js → index-qzdjd4ct.js} +3 -3
  40. package/{index-4zgs8g1w.js → index-rxkpk7n4.js} +1 -1
  41. package/{index-hjzr8ja1.js → index-xmmzf150.js} +1 -1
  42. package/{index-fmgbwsr5.js → index-y4vkyhvv.js} +4 -4
  43. package/{index-jtm86byd.js → index-z8ptfkzv.js} +84 -40
  44. package/index.js +20 -19
  45. package/{library.command-pwfx8dx2.js → library.command-cv9gdbmg.js} +5 -4
  46. package/{localRegistry.command-bxhqmd1d.js → localRegistry.command-d8fg36zc.js} +9 -8
  47. package/{module.command-0t3ewata.js → module.command-qyr1s9cn.js} +9 -8
  48. package/{package.command-7hmkxvpa.js → package.command-wsjx0k47.js} +5 -4
  49. package/package.json +2 -2
  50. package/{page.command-st6r4s0q.js → page.command-rdzg65rm.js} +5 -4
  51. package/{primitive.command-1qkxpzsn.js → primitive.command-89g38b53.js} +10 -9
  52. package/{quality.command-hy5vt1bh.js → quality.command-0cs2z92d.js} +459 -86
  53. package/{repair.command-nakkq2zp.js → repair.command-6fphxfwj.js} +7 -6
  54. package/{routeSourceValidator-cncd92fv.js → routeSourceValidator-e4jmbfcx.js} +2 -38
  55. package/{scalar.command-fjq272gs.js → scalar.command-xfer69s6.js} +8 -7
  56. package/templates/app/page/styles.css.template +3 -3
  57. package/templates/crudPages/[__model__Id]/edit/page.tsx +1 -1
  58. package/templates/crudPages/new/page.tsx +1 -1
  59. package/templates/crudSinglePage/page.tsx +1 -1
  60. package/templates/lib/db.ts +3 -1
  61. package/templates/lib/useClient.ts +9 -3
  62. package/templates/module/__Model__.Util.tsx +1 -1
  63. package/templates/module/__Model__.Zone.tsx +3 -5
  64. package/templates/workspaceRoot/AGENTS.md.template +0 -827
  65. package/templates/workspaceRoot/biome.json.template +3 -198
  66. package/templates/workspaceRoot/docs/AI-DEVELOPMENT.md.template +4 -1
  67. package/{workflow.command-jswbgk90.js → workflow.command-2ygfqt4f.js} +15 -14
  68. package/{workspace.command-p9r9nckg.js → workspace.command-vbgpjkf9.js} +37 -35
  69. package/templates/libRoot/base/baseLogic.ts +0 -14
  70. package/templates/libRoot/base/index.ts +0 -8
  71. package/templates/workspaceRoot/.cursor/rules/akan-page-routing.mdc.template +0 -43
  72. package/templates/workspaceRoot/.cursor/rules/akan-sync-conventions.mdc.template +0 -33
  73. package/templates/workspaceRoot/.cursor/rules/application-test-commands.mdc.template +0 -16
  74. package/templates/workspaceRoot/.cursor/rules/change-scope.mdc.template +0 -12
  75. package/templates/workspaceRoot/.cursor/rules/client-server-boundaries.mdc.template +0 -17
  76. package/templates/workspaceRoot/.cursor/rules/coding-comments.mdc.template +0 -25
  77. package/templates/workspaceRoot/.cursor/rules/coding-style.mdc.template +0 -44
  78. package/templates/workspaceRoot/.cursor/rules/domain-module-conventions.mdc.template +0 -17
  79. package/templates/workspaceRoot/.cursor/rules/format-lint.mdc.template +0 -12
  80. package/templates/workspaceRoot/.cursor/rules/lint-enforced-rules.mdc.template +0 -18
  81. package/templates/workspaceRoot/.cursor/rules/module-file-playbook.mdc.template +0 -61
  82. package/templates/workspaceRoot/.cursor/rules/naming-conventions.mdc.template +0 -15
  83. package/templates/workspaceRoot/.cursor/rules/react-component-style.mdc.template +0 -21
  84. package/templates/workspaceRoot/.cursor/rules/repo-overview.mdc.template +0 -17
  85. package/templates/workspaceRoot/.cursor/rules/scalar-modeling.mdc.template +0 -58
  86. package/templates/workspaceRoot/.cursor/rules/secrets-and-env.mdc.template +0 -13
  87. package/templates/workspaceRoot/.cursor/rules/service-signal-conventions.mdc.template +0 -83
  88. package/templates/workspaceRoot/.cursor/rules/srvkit-adapters-and-guards.mdc.template +0 -50
  89. package/templates/workspaceRoot/.cursor/rules/typescript-imports.mdc.template +0 -16
@@ -7,9 +7,13 @@ import {
7
7
  command,
8
8
  runner,
9
9
  script
10
- } from "./index-jk9q6pse.js";
11
- import"./index-jtm86byd.js";
12
- import"./index-6pz1j0zj.js";
10
+ } from "./index-kdh76j26.js";
11
+ import {
12
+ appRootAllowedFiles,
13
+ libFacetRootAllowedFiles
14
+ } from "./index-z8ptfkzv.js";
15
+ import"./index-mxvakhsm.js";
16
+ import"./index-1577bej2.js";
13
17
  import"./index-67546d0j.js";
14
18
  import"./index-r24hmh0q.js";
15
19
 
@@ -17,8 +21,371 @@ import"./index-r24hmh0q.js";
17
21
  import { createHash } from "crypto";
18
22
  import { readdir, readFile, stat } from "fs/promises";
19
23
  import path from "path";
24
+ import { RESERVED_ROUTE_CONFIG_EXPORTS } from "akanjs/common";
20
25
  import ignore from "ignore";
26
+ import ts2 from "typescript";
27
+
28
+ // pkgs/@akanjs/devkit/ssrScanner.ts
21
29
  import ts from "typescript";
30
+ var STATIC_COMPONENT_MIN_MASS = 4;
31
+ var MIXED_COMPONENT_MIN_MASS = 10;
32
+ var MIXED_COMPONENT_MAX_TOUCHES = 2;
33
+ var MODULE_SERVER_VIEW_MIN_CLIENT_MASS = 12;
34
+
35
+ class SsrScanner {
36
+ static #serverSafeCalls = new Set(["usePage", "getSelf", "useServer"]);
37
+ static #clientGlobals = new Set([
38
+ "window",
39
+ "document",
40
+ "navigator",
41
+ "localStorage",
42
+ "sessionStorage",
43
+ "location",
44
+ "history",
45
+ "screen",
46
+ "matchMedia",
47
+ "IntersectionObserver",
48
+ "ResizeObserver",
49
+ "MutationObserver",
50
+ "requestAnimationFrame",
51
+ "WebSocket"
52
+ ]);
53
+ static #clientRuntimeImports = new Set(["st", "fetch"]);
54
+ scan(sourceFiles) {
55
+ const componentFiles = sourceFiles.filter((sourceFile) => this.#isBalancedFile(sourceFile.file));
56
+ return {
57
+ warnings: [
58
+ ...componentFiles.flatMap((sourceFile) => this.#scanFile(sourceFile)),
59
+ ...this.#scanModules(componentFiles)
60
+ ],
61
+ balance: this.#measureBalance(componentFiles)
62
+ };
63
+ }
64
+ #scanFile(sourceFile) {
65
+ if (!this.#hasUseClient(sourceFile.sourceFile))
66
+ return [];
67
+ const vendorNames = this.#getVendorNames(sourceFile.sourceFile);
68
+ const hasVendorImport = this.#hasVendorImport(sourceFile.sourceFile);
69
+ const importsClientRuntime = this.#importsClientRuntime(sourceFile.sourceFile);
70
+ const components = this.#getComponents(sourceFile, vendorNames);
71
+ const warnings = [];
72
+ if (!hasVendorImport && !importsClientRuntime && this.#getTouches(sourceFile.sourceFile, sourceFile.sourceFile).length === 0 && !this.#isConventionClientFile(sourceFile.file)) {
73
+ warnings.push({
74
+ rule: "akan.ssr.unnecessary-use-client",
75
+ scope: "ssr",
76
+ severity: "warning",
77
+ file: sourceFile.file,
78
+ line: 1,
79
+ message: `"use client" is declared but the file uses no client-only capability (hook, event handler, store, or browser API).`
80
+ });
81
+ }
82
+ for (const component of components) {
83
+ if (component.vendorTags)
84
+ continue;
85
+ if (component.touches.length === 0 && component.mass >= STATIC_COMPONENT_MIN_MASS) {
86
+ warnings.push({
87
+ rule: "akan.ssr.client-static-component",
88
+ scope: "ssr",
89
+ severity: "warning",
90
+ file: sourceFile.file,
91
+ line: component.line,
92
+ message: `Client component "${component.name}" renders ${component.mass} JSX elements with no client-only capability. It is server-renderable markup sitting in the client bundle.`
93
+ });
94
+ continue;
95
+ }
96
+ if (component.touches.length >= 1 && component.touches.length <= MIXED_COMPONENT_MAX_TOUCHES && component.mass >= MIXED_COMPONENT_MIN_MASS) {
97
+ warnings.push({
98
+ rule: "akan.ssr.client-static-markup",
99
+ scope: "ssr",
100
+ severity: "warning",
101
+ file: sourceFile.file,
102
+ line: component.line,
103
+ message: `Client component "${component.name}" renders ${component.mass} JSX elements around only ${component.touches.length} client-only touch (${[...new Set(component.touches)].join(", ")}). Most of this subtree does not need the client bundle.`
104
+ });
105
+ }
106
+ }
107
+ warnings.push(...this.#getMountLoadWarnings(sourceFile));
108
+ warnings.push(...this.#getTemplateStateWarnings(sourceFile));
109
+ return warnings;
110
+ }
111
+ #scanModules(sourceFiles) {
112
+ const modules = new Map;
113
+ for (const sourceFile of sourceFiles) {
114
+ const moduleDir = this.#getModuleDir(sourceFile.file);
115
+ if (!moduleDir)
116
+ continue;
117
+ const entry = modules.get(moduleDir) ?? { clientMass: 0, serverFiles: 0, line: sourceFile.file };
118
+ if (this.#hasUseClient(sourceFile.sourceFile))
119
+ entry.clientMass += this.#getMass(sourceFile.sourceFile);
120
+ else if (/\.(Unit|View)\.tsx$/.test(sourceFile.file))
121
+ entry.serverFiles += 1;
122
+ modules.set(moduleDir, entry);
123
+ }
124
+ return [...modules].filter(([, entry]) => entry.serverFiles === 0 && entry.clientMass >= MODULE_SERVER_VIEW_MIN_CLIENT_MASS).map(([moduleDir, entry]) => ({
125
+ rule: "akan.ssr.module-missing-server-view",
126
+ scope: "ssr",
127
+ severity: "warning",
128
+ file: entry.line,
129
+ message: `Module "${moduleDir}" renders ${entry.clientMass} JSX elements from client files only; it declares no Unit or View server component.`
130
+ }));
131
+ }
132
+ #getMountLoadWarnings(sourceFile) {
133
+ const warnings = [];
134
+ const visit = (node) => {
135
+ if (this.#isMountEffect(sourceFile.sourceFile, node)) {
136
+ for (const load of this.#getLoadCalls(sourceFile.sourceFile, node)) {
137
+ warnings.push({
138
+ rule: "akan.ssr.client-mount-load",
139
+ scope: "ssr",
140
+ severity: "warning",
141
+ file: sourceFile.file,
142
+ line: this.#getLine(sourceFile.sourceFile, load.node),
143
+ message: `Mount-only effect loads server data with ${load.callee}(). The route can fetch this before the first byte instead.`
144
+ });
145
+ }
146
+ }
147
+ ts.forEachChild(node, visit);
148
+ };
149
+ ts.forEachChild(sourceFile.sourceFile, visit);
150
+ return warnings;
151
+ }
152
+ #isMountEffect(sourceFile, node) {
153
+ if (!ts.isCallExpression(node))
154
+ return false;
155
+ const callee = node.expression.getText(sourceFile);
156
+ if (callee !== "useEffect" && callee !== "useLayoutEffect")
157
+ return false;
158
+ const deps = node.arguments[1];
159
+ return !!deps && ts.isArrayLiteralExpression(deps) && deps.elements.length === 0;
160
+ }
161
+ #getLoadCalls(sourceFile, node) {
162
+ const calls = [];
163
+ const visit = (child) => {
164
+ if (ts.isCallExpression(child)) {
165
+ const callee = child.expression.getText(sourceFile);
166
+ if (/^fetch\.[a-z]/.test(callee) || /^st\.do\.(init|get|view|load|list|count|insight)[A-Z]/.test(callee))
167
+ calls.push({ callee, node: child });
168
+ }
169
+ ts.forEachChild(child, visit);
170
+ };
171
+ ts.forEachChild(node, visit);
172
+ return calls;
173
+ }
174
+ #getTemplateStateWarnings(sourceFile) {
175
+ if (!sourceFile.file.endsWith(".Template.tsx"))
176
+ return [];
177
+ const warnings = [];
178
+ const visit = (node) => {
179
+ if (ts.isCallExpression(node) && node.expression.getText(sourceFile.sourceFile) === "useState") {
180
+ warnings.push({
181
+ rule: "akan.ssr.template-client-state",
182
+ scope: "ssr",
183
+ severity: "warning",
184
+ file: sourceFile.file,
185
+ line: this.#getLine(sourceFile.sourceFile, node),
186
+ message: "Template holds form state in useState. Templates are store-driven and carry no local state."
187
+ });
188
+ }
189
+ ts.forEachChild(node, visit);
190
+ };
191
+ ts.forEachChild(sourceFile.sourceFile, visit);
192
+ return warnings;
193
+ }
194
+ #measureBalance(sourceFiles) {
195
+ const scopes = new Map;
196
+ for (const sourceFile of sourceFiles) {
197
+ const segments = sourceFile.file.split("/");
198
+ const scope = `${segments[0]}/${segments[1]}`;
199
+ const entry = scopes.get(scope) ?? { serverMass: 0, clientMass: 0 };
200
+ const mass = this.#getMass(sourceFile.sourceFile);
201
+ if (this.#hasUseClient(sourceFile.sourceFile))
202
+ entry.clientMass += mass;
203
+ else
204
+ entry.serverMass += mass;
205
+ scopes.set(scope, entry);
206
+ }
207
+ const entries = [...scopes].map(([scope, mass]) => ({ scope, ...mass, serverShare: getShare(mass.serverMass, mass.clientMass) })).sort((a, b) => a.scope.localeCompare(b.scope));
208
+ if (entries.length < 2)
209
+ return entries;
210
+ const serverMass = entries.reduce((sum, entry) => sum + entry.serverMass, 0);
211
+ const clientMass = entries.reduce((sum, entry) => sum + entry.clientMass, 0);
212
+ return [...entries, { scope: "workspace", serverMass, clientMass, serverShare: getShare(serverMass, clientMass) }];
213
+ }
214
+ #getComponents(sourceFile, vendorNames) {
215
+ const components = [];
216
+ for (const statement of sourceFile.sourceFile.statements) {
217
+ for (const { name, node } of this.#getComponentNodes(statement)) {
218
+ if (!/^[A-Z]/.test(name))
219
+ continue;
220
+ components.push({
221
+ name,
222
+ line: this.#getLine(sourceFile.sourceFile, node),
223
+ mass: this.#getMass(node),
224
+ touches: this.#getTouches(sourceFile.sourceFile, node),
225
+ vendorTags: [...this.#getTagNames(sourceFile.sourceFile, node)].some((tag) => vendorNames.has(tag))
226
+ });
227
+ }
228
+ }
229
+ return components;
230
+ }
231
+ #getComponentNodes(statement) {
232
+ if (ts.isFunctionDeclaration(statement) && statement.body)
233
+ return [{ name: statement.name?.text ?? "default", node: statement.body }];
234
+ if (!ts.isVariableStatement(statement))
235
+ return [];
236
+ return statement.declarationList.declarations.flatMap((declaration) => {
237
+ if (!ts.isIdentifier(declaration.name) || !declaration.initializer)
238
+ return [];
239
+ if (!ts.isArrowFunction(declaration.initializer) && !ts.isFunctionExpression(declaration.initializer))
240
+ return [];
241
+ return [{ name: declaration.name.text, node: declaration.initializer }];
242
+ });
243
+ }
244
+ #getTouches(sourceFile, node) {
245
+ const touches = [];
246
+ const visit = (child) => {
247
+ if (ts.isCallExpression(child)) {
248
+ const callee = child.expression.getText(sourceFile);
249
+ const bareName = callee.split(".").pop() ?? callee;
250
+ if (callee === "createContext" || callee === "lazy")
251
+ touches.push(callee);
252
+ else if (/^use[A-Z]/.test(bareName) && !SsrScanner.#serverSafeCalls.has(bareName))
253
+ touches.push(bareName);
254
+ }
255
+ if (ts.isJsxAttribute(child) && /^on[A-Z]/.test(child.name.getText(sourceFile)))
256
+ touches.push(child.name.getText(sourceFile));
257
+ if (ts.isPropertyAccessExpression(child)) {
258
+ const root = getAccessRoot(child);
259
+ if (root === "st")
260
+ touches.push("st");
261
+ else if (SsrScanner.#clientGlobals.has(root))
262
+ touches.push(root);
263
+ }
264
+ ts.forEachChild(child, visit);
265
+ };
266
+ ts.forEachChild(node, visit);
267
+ return touches;
268
+ }
269
+ #getTagNames(sourceFile, node) {
270
+ const tags = new Set;
271
+ const visit = (child) => {
272
+ if (ts.isJsxOpeningElement(child) || ts.isJsxSelfClosingElement(child))
273
+ tags.add(child.tagName.getText(sourceFile).split(".")[0]);
274
+ ts.forEachChild(child, visit);
275
+ };
276
+ ts.forEachChild(node, visit);
277
+ return tags;
278
+ }
279
+ #getMass(node) {
280
+ let mass = 0;
281
+ const visit = (child) => {
282
+ if (ts.isJsxOpeningElement(child) || ts.isJsxSelfClosingElement(child))
283
+ mass += 1;
284
+ ts.forEachChild(child, visit);
285
+ };
286
+ ts.forEachChild(node, visit);
287
+ return mass;
288
+ }
289
+ #hasUseClient(sourceFile) {
290
+ const first = sourceFile.statements[0];
291
+ if (!first || !ts.isExpressionStatement(first) || !ts.isStringLiteral(first.expression))
292
+ return false;
293
+ return first.expression.text === "use client";
294
+ }
295
+ #hasVendorImport(sourceFile) {
296
+ return sourceFile.statements.some((statement) => ts.isImportDeclaration(statement) && isVendorSpecifier(getSpecifier(statement)));
297
+ }
298
+ #getVendorNames(sourceFile) {
299
+ const names = new Set;
300
+ for (const statement of sourceFile.statements) {
301
+ if (!ts.isImportDeclaration(statement) || !isVendorSpecifier(getSpecifier(statement)))
302
+ continue;
303
+ const clause = statement.importClause;
304
+ if (clause?.name)
305
+ names.add(clause.name.text);
306
+ if (clause?.namedBindings && ts.isNamespaceImport(clause.namedBindings))
307
+ names.add(clause.namedBindings.name.text);
308
+ if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings))
309
+ for (const element of clause.namedBindings.elements)
310
+ names.add(element.name.text);
311
+ }
312
+ return names;
313
+ }
314
+ #importsClientRuntime(sourceFile) {
315
+ for (const statement of sourceFile.statements) {
316
+ if (!ts.isImportDeclaration(statement))
317
+ continue;
318
+ const bindings = statement.importClause?.namedBindings;
319
+ if (!bindings || !ts.isNamedImports(bindings))
320
+ continue;
321
+ if (bindings.elements.some((element) => SsrScanner.#clientRuntimeImports.has(element.name.text)))
322
+ return true;
323
+ }
324
+ return false;
325
+ }
326
+ #isConventionClientFile(file) {
327
+ if (file.endsWith("/index_.tsx"))
328
+ return true;
329
+ return /\.(Zone|Template|Util)\.tsx$/.test(file) && this.#getModuleDir(file) !== null;
330
+ }
331
+ #isBalancedFile(file) {
332
+ if (!file.endsWith(".tsx") || file.endsWith(".test.tsx") || file.endsWith(".spec.tsx"))
333
+ return false;
334
+ const segments = file.split("/");
335
+ if (segments[0] !== "apps" && segments[0] !== "libs")
336
+ return false;
337
+ return segments[2] === "ui" || segments[2] === "lib";
338
+ }
339
+ #getModuleDir(file) {
340
+ const segments = file.split("/");
341
+ const libIndex = segments.indexOf("lib");
342
+ if (libIndex < 1 || segments.length <= libIndex + 2)
343
+ return null;
344
+ const moduleName = segments[libIndex + 1];
345
+ if (moduleName.startsWith("_"))
346
+ return null;
347
+ return segments.slice(0, libIndex + 2).join("/");
348
+ }
349
+ #getLine(sourceFile, node) {
350
+ return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
351
+ }
352
+ }
353
+ var SSR_SERVER_SHARE_TARGET = 0.5;
354
+ function formatSsrBalance(balance) {
355
+ if (balance.length === 0)
356
+ return ["No component files found."];
357
+ return balance.map((entry) => {
358
+ const total = entry.serverMass + entry.clientMass;
359
+ const share = `${Math.round(entry.serverShare * 100)}% server`;
360
+ const counts = `${entry.serverMass} of ${total} JSX elements, ${entry.clientMass} client`;
361
+ const flag = entry.serverShare < SSR_SERVER_SHARE_TARGET ? ` <- below the ${Math.round(SSR_SERVER_SHARE_TARGET * 100)}% target` : "";
362
+ return ` ${entry.scope}: ${share} (${counts})${flag}`;
363
+ });
364
+ }
365
+ function getShare(serverMass, clientMass) {
366
+ const total = serverMass + clientMass;
367
+ return total === 0 ? 1 : serverMass / total;
368
+ }
369
+ function getSpecifier(statement) {
370
+ return ts.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : "";
371
+ }
372
+ function isVendorSpecifier(specifier) {
373
+ if (specifier === "" || specifier.startsWith(".") || specifier.startsWith("/"))
374
+ return false;
375
+ if (specifier === "react" || specifier === "react-dom" || specifier.startsWith("react/"))
376
+ return false;
377
+ if (specifier.startsWith("node:"))
378
+ return false;
379
+ return !/^(akanjs|@akanjs|@libs|@apps|@contract)(\/|$)/.test(specifier);
380
+ }
381
+ function getAccessRoot(node) {
382
+ let current = node;
383
+ while (ts.isPropertyAccessExpression(current))
384
+ current = current.expression;
385
+ return ts.isIdentifier(current) ? current.text : "";
386
+ }
387
+
388
+ // pkgs/@akanjs/devkit/qualityScanner.ts
22
389
  var MAX_FILE_LINES = 2000;
23
390
  var PLACEHOLDER_EXPORT_NAMES = new Set([
24
391
  "aa",
@@ -42,27 +409,6 @@ var SUGGESTED_RULES = [
42
409
  "Move shared app utilities to apps/*/common instead of creating apps/*/base.",
43
410
  "Avoid large mixed-purpose class files; class export files should import helpers from neighboring utility files instead of declaring them inline."
44
411
  ];
45
- var APP_ROOT_FILES = new Set([
46
- "akan.app.json",
47
- "akan.config.ts",
48
- "capacitor.config.ts",
49
- "client.ts",
50
- "main.ts",
51
- "package.json",
52
- "server.ts",
53
- "tsconfig.json"
54
- ]);
55
- var LIB_ROOT_FILES = new Set([
56
- "cnst.ts",
57
- "db.ts",
58
- "dict.ts",
59
- "option.ts",
60
- "sig.ts",
61
- "srv.ts",
62
- "st.ts",
63
- "useClient.ts",
64
- "useServer.ts"
65
- ]);
66
412
  var CONVENTION_SUFFIXES = [
67
413
  ".constant.ts",
68
414
  ".dictionary.ts",
@@ -71,20 +417,6 @@ var CONVENTION_SUFFIXES = [
71
417
  ".signal.ts",
72
418
  ".store.ts"
73
419
  ];
74
- var PAGE_RESERVED_EXPORTS = new Set([
75
- "pageConfig",
76
- "head",
77
- "metadata",
78
- "generateHead",
79
- "generateMetadata",
80
- "fonts",
81
- "manifest",
82
- "theme",
83
- "reconnect",
84
- "wsConnect",
85
- "layoutStyle",
86
- "gaTrackingId"
87
- ]);
88
420
  var RULE_FIXES = {
89
421
  "akan.global.duplicate-exported-function-name": "Rename one of the exports, or if they are the same thing, extract it into one shared module and import it in both places.",
90
422
  "akan.global.duplicate-exported-function-body": "Extract the shared implementation into a single exported helper and import it, instead of copying the body.",
@@ -101,7 +433,13 @@ var RULE_FIXES = {
101
433
  "akan.file.component-export": "Move the value or type to a util/constant/type file in ui/, webkit/, or common/ and import it. Adding `export` is not a valid fix \u2014 only PascalCase components and their `<Component>Props` interface belong here.",
102
434
  "akan.layout.app-root-file": "Move the file into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit).",
103
435
  "akan.layout.lib-root-file": "Move the file into a domain module folder under lib/; keep lib root limited to generated support facets.",
104
- "akan.layout.module-ui-file": "Rename the file to an allowed module UI name, or move it to ui/ if it is not a module component."
436
+ "akan.layout.module-ui-file": "Rename the file to an allowed module UI name, or move it to ui/ if it is not a module component.",
437
+ "akan.ssr.unnecessary-use-client": 'Delete the "use client" directive so the file renders on the server. If it exists only to wrap one client child, drop the wrapper and use the child directly.',
438
+ "akan.ssr.client-static-component": "Move the component to a server file \u2014 a <Model>.Unit.tsx / <Model>.View.tsx for a module, or a ui/ file with no directive \u2014 and reference it from the client file.",
439
+ "akan.ssr.client-static-markup": "Keep the interactive element in the client component and hoist the static subtree into a server component, then accept it as `children` or render it through a Unit/View reference.",
440
+ "akan.ssr.client-mount-load": "Load the data in the route with `fetch.initX(...)` / `fetch.viewX(...)` and pass the init/view object down as a prop; the client store hydrates from it and the effect goes away.",
441
+ "akan.ssr.module-missing-server-view": "Add a <Model>.Unit.tsx for list/card rendering and a <Model>.View.tsx for the detail surface, then have the Zone delegate to them.",
442
+ "akan.ssr.template-client-state": "Bind the field to the store instead: `value={xForm.field}` with `onChange={st.do.setFieldOnX}`."
105
443
  };
106
444
  function getRuleFix(rule) {
107
445
  if (rule.startsWith("akan.convention"))
@@ -114,18 +452,21 @@ class AkanQualityScanner {
114
452
  const targetFiles = await this.#collectTargetFiles(workspaceRoot);
115
453
  const sourceFiles = await Promise.all(targetFiles.filter((file) => !AbstractDoc.isAbstractPath(file)).map((file) => this.#readSourceFile(workspaceRoot, file)));
116
454
  const abstractFiles = await Promise.all(targetFiles.filter((file) => AbstractDoc.isAbstractPath(file)).map((file) => this.#readTextFile(workspaceRoot, file)));
455
+ const ssr = new SsrScanner().scan(sourceFiles);
117
456
  const warnings = [
118
457
  ...this.#scanGlobalQuality(sourceFiles),
119
458
  ...sourceFiles.flatMap((sourceFile) => this.#scanSingleFileQuality(sourceFile)),
120
459
  ...sourceFiles.flatMap((sourceFile) => this.#scanComponentQuality(sourceFile)),
121
460
  ...sourceFiles.flatMap((sourceFile) => this.#scanConventionQuality(sourceFile)),
122
461
  ...sourceFiles.flatMap((sourceFile) => this.#scanLayoutQuality(sourceFile)),
123
- ...abstractFiles.flatMap((abstractFile) => this.#scanAbstractQuality(abstractFile))
462
+ ...abstractFiles.flatMap((abstractFile) => this.#scanAbstractQuality(abstractFile)),
463
+ ...ssr.warnings
124
464
  ];
125
465
  return {
126
466
  workspaceRoot,
127
467
  scannedFiles: sourceFiles.length + abstractFiles.length,
128
468
  warnings: warnings.map((warning) => ({ ...warning, fix: warning.fix ?? getRuleFix(warning.rule) })).sort(compareWarnings),
469
+ ssrBalance: ssr.balance,
129
470
  suggestedRules: SUGGESTED_RULES
130
471
  };
131
472
  }
@@ -171,7 +512,7 @@ class AkanQualityScanner {
171
512
  file,
172
513
  absolutePath,
173
514
  content,
174
- sourceFile: ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true, getScriptKind(file))
515
+ sourceFile: ts2.createSourceFile(file, content, ts2.ScriptTarget.Latest, true, getScriptKind(file))
175
516
  };
176
517
  }
177
518
  async#readTextFile(workspaceRoot, file) {
@@ -332,7 +673,7 @@ class AkanQualityScanner {
332
673
  #scanLayoutQuality(sourceFile) {
333
674
  const segments = sourceFile.file.split("/");
334
675
  const warnings = [];
335
- if (segments[0] === "apps" && segments.length === 3 && !APP_ROOT_FILES.has(segments[2])) {
676
+ if (segments[0] === "apps" && segments.length === 3 && !appRootAllowedFiles.has(segments[2])) {
336
677
  warnings.push({
337
678
  rule: "akan.layout.app-root-file",
338
679
  scope: "layout",
@@ -342,7 +683,7 @@ class AkanQualityScanner {
342
683
  });
343
684
  }
344
685
  const libRootFile = getLibRootFile(sourceFile.file);
345
- if (libRootFile && !LIB_ROOT_FILES.has(libRootFile)) {
686
+ if (libRootFile && !libFacetRootAllowedFiles.has(libRootFile)) {
346
687
  warnings.push({
347
688
  rule: "akan.layout.lib-root-file",
348
689
  scope: "layout",
@@ -368,6 +709,10 @@ function formatQualityScanResult(result) {
368
709
  "",
369
710
  ...formatQualityWarnings(result.warnings),
370
711
  "",
712
+ "SSR balance (component files, JSX elements rendered per side):",
713
+ "",
714
+ ...formatSsrBalance(result.ssrBalance),
715
+ "",
371
716
  "Suggested quality rules:",
372
717
  "",
373
718
  ...result.suggestedRules.map((rule) => ` - ${rule}`)
@@ -375,6 +720,24 @@ function formatQualityScanResult(result) {
375
720
  return sections.join(`
376
721
  `);
377
722
  }
723
+ function formatSsrScanResult(result) {
724
+ const sections = [
725
+ "Akan SSR Balance Scan",
726
+ `workspace: ${result.workspaceRoot}`,
727
+ `scanned files: ${result.scannedFiles}`,
728
+ `ssr warnings: ${result.warnings.length}`,
729
+ "",
730
+ "Server render share (component files, JSX elements rendered per side):",
731
+ "",
732
+ ...formatSsrBalance(result.ssrBalance),
733
+ "",
734
+ "Warnings:",
735
+ "",
736
+ ...formatQualityWarnings(result.warnings)
737
+ ];
738
+ return sections.join(`
739
+ `);
740
+ }
378
741
  function formatQualityWarnings(warnings) {
379
742
  if (warnings.length === 0)
380
743
  return ["No warnings found."];
@@ -396,7 +759,7 @@ function getExportedFunctionLikes(sourceFile) {
396
759
  const declarations = [];
397
760
  const nameExempt = isPageRouteFile(sourceFile.file) || isUiComponentFile(sourceFile.file);
398
761
  for (const statement of sourceFile.sourceFile.statements) {
399
- if (ts.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
762
+ if (ts2.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
400
763
  declarations.push({
401
764
  name: statement.name.text,
402
765
  kind: "function",
@@ -406,7 +769,7 @@ function getExportedFunctionLikes(sourceFile) {
406
769
  duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false)
407
770
  });
408
771
  }
409
- if (ts.isClassDeclaration(statement) && statement.name && isExported(statement)) {
772
+ if (ts2.isClassDeclaration(statement) && statement.name && isExported(statement)) {
410
773
  declarations.push({
411
774
  name: statement.name.text,
412
775
  kind: "class",
@@ -416,9 +779,9 @@ function getExportedFunctionLikes(sourceFile) {
416
779
  duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, isEnumClassStatement(sourceFile.sourceFile, statement))
417
780
  });
418
781
  }
419
- if (ts.isVariableStatement(statement) && isExported(statement)) {
782
+ if (ts2.isVariableStatement(statement) && isExported(statement)) {
420
783
  for (const declaration of statement.declarationList.declarations) {
421
- if (!ts.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
784
+ if (!ts2.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
422
785
  continue;
423
786
  declarations.push({
424
787
  name: declaration.name.text,
@@ -457,14 +820,14 @@ function isInLibModule(file) {
457
820
  return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
458
821
  }
459
822
  function isEnumClassStatement(sourceFile, statement) {
460
- if (!ts.isClassDeclaration(statement))
823
+ if (!ts2.isClassDeclaration(statement))
461
824
  return false;
462
- const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword);
825
+ const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts2.SyntaxKind.ExtendsKeyword);
463
826
  const expression = heritageClause?.types[0]?.expression;
464
827
  return !!expression && expression.getText(sourceFile).startsWith("enumOf(");
465
828
  }
466
829
  function getExportedClassNames(sourceFile) {
467
- return sourceFile.statements.filter((statement) => ts.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
830
+ return sourceFile.statements.filter((statement) => ts2.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
468
831
  }
469
832
  function isComponentDeclarationFile(file) {
470
833
  if (!file.endsWith(".tsx"))
@@ -480,7 +843,7 @@ function isComponentDeclarationFile(file) {
480
843
  function getComponentFileDeclarations(sourceFile) {
481
844
  const reExportedNames = new Set;
482
845
  for (const statement of sourceFile.statements) {
483
- if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
846
+ if (ts2.isExportDeclaration(statement) && statement.exportClause && ts2.isNamedExports(statement.exportClause)) {
484
847
  for (const element of statement.exportClause.elements)
485
848
  reExportedNames.add((element.propertyName ?? element.name).text);
486
849
  }
@@ -490,19 +853,19 @@ function getComponentFileDeclarations(sourceFile) {
490
853
  const isDefaultExport = isDefaultExportStatement(statement);
491
854
  const inlineExported = isExported(statement);
492
855
  const add = (name, kind, line) => declarations.push({ name, kind, line, exported: inlineExported || reExportedNames.has(name), isDefaultExport });
493
- if (ts.isInterfaceDeclaration(statement))
856
+ if (ts2.isInterfaceDeclaration(statement))
494
857
  add(statement.name.text, "interface", getLine(sourceFile, statement));
495
- else if (ts.isTypeAliasDeclaration(statement))
858
+ else if (ts2.isTypeAliasDeclaration(statement))
496
859
  add(statement.name.text, "type", getLine(sourceFile, statement));
497
- else if (ts.isEnumDeclaration(statement))
860
+ else if (ts2.isEnumDeclaration(statement))
498
861
  add(statement.name.text, "enum", getLine(sourceFile, statement));
499
- else if (ts.isFunctionDeclaration(statement) && statement.name)
862
+ else if (ts2.isFunctionDeclaration(statement) && statement.name)
500
863
  add(statement.name.text, "function", getLine(sourceFile, statement));
501
- else if (ts.isClassDeclaration(statement) && statement.name)
864
+ else if (ts2.isClassDeclaration(statement) && statement.name)
502
865
  add(statement.name.text, "class", getLine(sourceFile, statement));
503
- else if (ts.isVariableStatement(statement)) {
866
+ else if (ts2.isVariableStatement(statement)) {
504
867
  for (const declaration of statement.declarationList.declarations) {
505
- if (!ts.isIdentifier(declaration.name))
868
+ if (!ts2.isIdentifier(declaration.name))
506
869
  continue;
507
870
  const kind = isFunctionLikeInitializer(declaration.initializer) ? "function" : "variable";
508
871
  add(declaration.name.text, kind, getLine(sourceFile, declaration));
@@ -514,15 +877,15 @@ function getComponentFileDeclarations(sourceFile) {
514
877
  function getCompoundComponentNames(sourceFile) {
515
878
  const names = new Set;
516
879
  for (const statement of sourceFile.statements) {
517
- if (!ts.isExpressionStatement(statement))
880
+ if (!ts2.isExpressionStatement(statement))
518
881
  continue;
519
882
  const { expression } = statement;
520
- if (!ts.isBinaryExpression(expression) || expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
883
+ if (!ts2.isBinaryExpression(expression) || expression.operatorToken.kind !== ts2.SyntaxKind.EqualsToken)
521
884
  continue;
522
- if (!ts.isPropertyAccessExpression(expression.left) || !isPascalCaseName(expression.left.name.text))
885
+ if (!ts2.isPropertyAccessExpression(expression.left) || !isPascalCaseName(expression.left.name.text))
523
886
  continue;
524
887
  names.add(expression.left.name.text);
525
- if (ts.isIdentifier(expression.right) && isPascalCaseName(expression.right.text))
888
+ if (ts2.isIdentifier(expression.right) && isPascalCaseName(expression.right.text))
526
889
  names.add(expression.right.text);
527
890
  }
528
891
  return names;
@@ -536,15 +899,15 @@ function isRestrictedInternalKind(kind) {
536
899
  function isAllowedComponentExport(declaration, isPage) {
537
900
  if (isComponentValueKind(declaration.kind) && isPascalCaseName(declaration.name))
538
901
  return true;
539
- return isPage && PAGE_RESERVED_EXPORTS.has(declaration.name);
902
+ return isPage && RESERVED_ROUTE_CONFIG_EXPORTS.has(declaration.name);
540
903
  }
541
904
  function isPascalCaseName(name) {
542
905
  return /^[A-Z]/.test(name) && !/^[A-Z0-9_]+$/.test(name);
543
906
  }
544
907
  function isDefaultExportStatement(statement) {
545
- if (ts.isExportAssignment(statement))
908
+ if (ts2.isExportAssignment(statement))
546
909
  return !statement.isExportEquals;
547
- return !!(ts.getCombinedModifierFlags(statement) & ts.ModifierFlags.Default);
910
+ return !!(ts2.getCombinedModifierFlags(statement) & ts2.ModifierFlags.Default);
548
911
  }
549
912
  function getPlaceholderExportWarnings(sourceFile) {
550
913
  if (!sourceFile.file.endsWith("/index.ts") && !sourceFile.file.endsWith("/index.tsx"))
@@ -574,7 +937,7 @@ function getDictionaryTextWarnings(sourceFile) {
574
937
  function getGlobalMutationWarnings(sourceFile) {
575
938
  const warnings = [];
576
939
  for (const statement of sourceFile.sourceFile.statements) {
577
- if (ts.isModuleDeclaration(statement) && statement.name.getText(sourceFile.sourceFile) === "global") {
940
+ if (ts2.isModuleDeclaration(statement) && statement.name.getText(sourceFile.sourceFile) === "global") {
578
941
  warnings.push({
579
942
  rule: "akan.file.global-declaration",
580
943
  scope: "file",
@@ -584,7 +947,7 @@ function getGlobalMutationWarnings(sourceFile) {
584
947
  message: "Global declarations require an explicit low-level integration allowlist."
585
948
  });
586
949
  }
587
- if (ts.isInterfaceDeclaration(statement) && statement.name.text === "Window") {
950
+ if (ts2.isInterfaceDeclaration(statement) && statement.name.text === "Window") {
588
951
  warnings.push({
589
952
  rule: "akan.file.window-augmentation",
590
953
  scope: "file",
@@ -594,7 +957,7 @@ function getGlobalMutationWarnings(sourceFile) {
594
957
  message: "Window augmentation should be isolated to approved browser integration files."
595
958
  });
596
959
  }
597
- if (ts.isExpressionStatement(statement) && statement.expression.getText(sourceFile.sourceFile).includes(".prototype.")) {
960
+ if (ts2.isExpressionStatement(statement) && statement.expression.getText(sourceFile.sourceFile).includes(".prototype.")) {
598
961
  warnings.push({
599
962
  rule: "akan.file.prototype-mutation",
600
963
  scope: "file",
@@ -612,23 +975,23 @@ function getTopLevelDeclarations(sourceFile) {
612
975
  }
613
976
  function getTopLevelDeclaration(sourceFile, statement) {
614
977
  const line = getLine(sourceFile, statement);
615
- if (ts.isClassDeclaration(statement) && statement.name) {
978
+ if (ts2.isClassDeclaration(statement) && statement.name) {
616
979
  return [{ name: statement.name.text, kind: "class", line, exported: isExported(statement), node: statement }];
617
980
  }
618
- if (ts.isFunctionDeclaration(statement) && statement.name) {
981
+ if (ts2.isFunctionDeclaration(statement) && statement.name) {
619
982
  return [{ name: statement.name.text, kind: "function", line, exported: isExported(statement), node: statement }];
620
983
  }
621
- if (ts.isInterfaceDeclaration(statement)) {
984
+ if (ts2.isInterfaceDeclaration(statement)) {
622
985
  return [{ name: statement.name.text, kind: "interface", line, exported: isExported(statement), node: statement }];
623
986
  }
624
- if (ts.isTypeAliasDeclaration(statement)) {
987
+ if (ts2.isTypeAliasDeclaration(statement)) {
625
988
  return [{ name: statement.name.text, kind: "type", line, exported: isExported(statement), node: statement }];
626
989
  }
627
- if (ts.isEnumDeclaration(statement)) {
990
+ if (ts2.isEnumDeclaration(statement)) {
628
991
  return [{ name: statement.name.text, kind: "enum", line, exported: isExported(statement), node: statement }];
629
992
  }
630
- if (ts.isVariableStatement(statement)) {
631
- return statement.declarationList.declarations.filter((declaration) => ts.isIdentifier(declaration.name)).map((declaration) => ({
993
+ if (ts2.isVariableStatement(statement)) {
994
+ return statement.declarationList.declarations.filter((declaration) => ts2.isIdentifier(declaration.name)).map((declaration) => ({
632
995
  name: declaration.name.text,
633
996
  kind: "variable",
634
997
  line: getLine(sourceFile, declaration),
@@ -636,7 +999,7 @@ function getTopLevelDeclaration(sourceFile, statement) {
636
999
  node: statement
637
1000
  }));
638
1001
  }
639
- if (ts.isExportDeclaration(statement)) {
1002
+ if (ts2.isExportDeclaration(statement)) {
640
1003
  return [{ name: "export declaration", kind: "export", line, exported: true, node: statement }];
641
1004
  }
642
1005
  return [];
@@ -661,9 +1024,9 @@ function isAllowedConstantDeclaration(modelName, declaration) {
661
1024
  return false;
662
1025
  if ([`${modelName}Input`, `${modelName}Object`, modelName, `Light${modelName}`, `${modelName}Insight`].includes(declaration.name))
663
1026
  return true;
664
- if (!ts.isClassDeclaration(declaration.node))
1027
+ if (!ts2.isClassDeclaration(declaration.node))
665
1028
  return false;
666
- const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword);
1029
+ const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token === ts2.SyntaxKind.ExtendsKeyword);
667
1030
  const expression = heritageClause?.types[0]?.expression;
668
1031
  return !!expression && expression.getText().startsWith("enumOf(");
669
1032
  }
@@ -729,13 +1092,13 @@ function getModuleInfo(file) {
729
1092
  return { moduleName, fileName, kind: "database" };
730
1093
  }
731
1094
  function isExportedConst(declaration) {
732
- return declaration.exported && ts.isVariableStatement(declaration.node) && (declaration.node.declarationList.flags & ts.NodeFlags.Const) !== 0;
1095
+ return declaration.exported && ts2.isVariableStatement(declaration.node) && (declaration.node.declarationList.flags & ts2.NodeFlags.Const) !== 0;
733
1096
  }
734
1097
  function isExported(node) {
735
- return !!ts.getCombinedModifierFlags(node) && !!(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export);
1098
+ return !!ts2.getCombinedModifierFlags(node) && !!(ts2.getCombinedModifierFlags(node) & ts2.ModifierFlags.Export);
736
1099
  }
737
1100
  function isFunctionLikeInitializer(node) {
738
- return !!node && (ts.isArrowFunction(node) || ts.isFunctionExpression(node));
1101
+ return !!node && (ts2.isArrowFunction(node) || ts2.isFunctionExpression(node));
739
1102
  }
740
1103
  function getBodyFingerprint(sourceFile, node) {
741
1104
  if (!node)
@@ -757,7 +1120,7 @@ async function isDirectory(absolutePath) {
757
1120
  }
758
1121
  }
759
1122
  function getScriptKind(file) {
760
- return file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
1123
+ return file.endsWith(".tsx") ? ts2.ScriptKind.TSX : ts2.ScriptKind.TS;
761
1124
  }
762
1125
  function getLine(sourceFile, node) {
763
1126
  return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
@@ -810,6 +1173,13 @@ class QualityScript extends script("quality", [QualityRunner]) {
810
1173
  spinner.succeed(`Quality scan completed (${result.scannedFiles} files, ${result.warnings.length} warnings)`);
811
1174
  Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatQualityScanResult(result));
812
1175
  }
1176
+ async ssr(workspace, format = "text") {
1177
+ const spinner = workspace.spinning("Measuring Akan server/client render balance...");
1178
+ const scanned = await this.qualityRunner.scan(workspace);
1179
+ const result = { ...scanned, warnings: scanned.warnings.filter((warning) => warning.scope === "ssr") };
1180
+ spinner.succeed(`SSR scan completed (${result.scannedFiles} files, ${result.warnings.length} warnings)`);
1181
+ Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSsrScanResult(result));
1182
+ }
813
1183
  }
814
1184
 
815
1185
  // pkgs/@akanjs/cli/quality/quality.command.ts
@@ -817,15 +1187,18 @@ class QualityCommand extends command("quality", [QualityScript], ({ public: targ
817
1187
  quality: target({ desc: "Scan apps and libs for Akan code quality warnings" }).arg("action", String, {
818
1188
  desc: "quality action",
819
1189
  default: "scan",
820
- enum: ["scan"]
1190
+ enum: ["scan", "ssr"]
821
1191
  }).option("format", String, {
822
1192
  desc: "output format",
823
1193
  default: "text",
824
1194
  enum: ["text", "json"]
825
1195
  }).with(Workspace).exec(async function(action, format, workspace) {
826
- if (action !== "scan")
827
- throw new Error(`Unknown quality action: ${action}. Use "scan".`);
828
- await this.qualityScript.scan(workspace, format);
1196
+ if (action === "scan")
1197
+ await this.qualityScript.scan(workspace, format);
1198
+ else if (action === "ssr")
1199
+ await this.qualityScript.ssr(workspace, format);
1200
+ else
1201
+ throw new Error(`Unknown quality action: ${action}. Use "scan" or "ssr".`);
829
1202
  })
830
1203
  })) {
831
1204
  }