@akanjs/cli 3.0.0-alpha.4 → 3.0.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.build-stamp +1 -1
- package/{abstractCompactor-rk6twzvj.js → abstractCompactor-e5yq60qz.js} +2 -2
- package/{agent.command-jfz8yfwy.js → agent.command-b0fcx3r2.js} +7 -7
- package/{application.command-mxyqtd6p.js → application.command-cgmy00gd.js} +5 -5
- package/{applicationBuildRunner-cwk52sb7.js → applicationBuildRunner-n1svrwgt.js} +4 -4
- package/{applicationReleasePackager-yrrhbkv3.js → applicationReleasePackager-gg8ccbeh.js} +1 -1
- package/buildBatch.proc.js +4 -4
- package/{capacitorApp-j357ak1g.js → capacitorApp-yyw35vg4.js} +3 -5
- package/{cloud.command-f87dwe52.js → cloud.command-1m7hfdpk.js} +9 -9
- package/{context.command-z5bf8bn7.js → context.command-b5heat2a.js} +15 -15
- package/{guideline.command-1m1ctar3.js → guideline.command-9ew6aqhm.js} +5 -5
- package/guidelines/ssrRule/ssrRule.generate.json +31 -0
- package/guidelines/ssrRule/ssrRule.instruction.md +108 -0
- package/incrementalBuilder.proc.js +3 -3
- package/{index-3pgtw35d.js → index-0cj2zxbm.js} +1 -1
- package/{index-cap2k2ch.js → index-0fj4yg7k.js} +52 -17
- package/{index-jtm86byd.js → index-47qxw1tz.js} +28 -22
- package/{index-vhv3xnjz.js → index-60a3jfcz.js} +6 -6
- package/{index-b978vs2e.js → index-6npsfpxr.js} +3 -3
- package/{index-csqk9z5j.js → index-c94nq3c1.js} +3 -3
- package/{index-cn24dcqn.js → index-exhpp31y.js} +2 -2
- package/{index-hjzr8ja1.js → index-hrwzz2wf.js} +1 -1
- package/{index-rh16j3c7.js → index-j9cxndc0.js} +3 -2
- package/{index-jk9q6pse.js → index-jkynkyfk.js} +1 -1
- package/{index-4yp06pn1.js → index-jn9vfpwz.js} +9 -31
- package/{index-4zgs8g1w.js → index-m4kpmmtj.js} +1 -1
- package/{index-e3qskyt2.js → index-meh7mmz9.js} +2 -2
- package/{index-ybybkec0.js → index-mn96y1nk.js} +6 -6
- package/{index-y0k42k04.js → index-mterpj70.js} +17 -86
- package/{index-6pz1j0zj.js → index-mxvakhsm.js} +6 -2
- package/{index-n0z3f2xg.js → index-nfxq39kj.js} +4 -4
- package/{index-tq1y56tp.js → index-nx1vbtsc.js} +35 -12
- package/{index-1s71fwrq.js → index-p69fcwe1.js} +11 -11
- package/{index-xj8bks3p.js → index-p6txr1ss.js} +4 -4
- package/{index-r4w8cry5.js → index-pfrt7qwh.js} +4 -4
- package/{index-xwgm94zr.js → index-qp089jp0.js} +1 -1
- package/{index-fmgbwsr5.js → index-wrrb8rxk.js} +4 -4
- package/index.js +19 -19
- package/{library.command-pwfx8dx2.js → library.command-aa1g8nsa.js} +4 -4
- package/{localRegistry.command-r4zjx5py.js → localRegistry.command-kshva1ck.js} +8 -8
- package/{module.command-0t3ewata.js → module.command-b1zzgqde.js} +8 -8
- package/{package.command-7hmkxvpa.js → package.command-9qv6vr9s.js} +4 -4
- package/package.json +2 -2
- package/{page.command-st6r4s0q.js → page.command-k16pw244.js} +4 -4
- package/{primitive.command-1qkxpzsn.js → primitive.command-s60dn1ae.js} +9 -9
- package/{quality.command-hy5vt1bh.js → quality.command-p3vaze52.js} +456 -71
- package/{repair.command-nakkq2zp.js → repair.command-mfv749ha.js} +6 -6
- package/{scalar.command-fjq272gs.js → scalar.command-hs3nwzv1.js} +7 -7
- package/templates/lib/db.ts +3 -1
- package/templates/lib/useClient.ts +9 -3
- package/templates/workspaceRoot/.cursor/rules/akan-sync-conventions.mdc.template +3 -2
- package/templates/workspaceRoot/.cursor/rules/client-server-boundaries.mdc.template +1 -0
- package/templates/workspaceRoot/.cursor/rules/ssr-first.mdc.template +59 -0
- package/templates/workspaceRoot/AGENTS.md.template +161 -25
- package/templates/workspaceRoot/biome.json.template +22 -19
- package/templates/workspaceRoot/docs/AI-DEVELOPMENT.md.template +1 -4
- package/{workflow.command-jswbgk90.js → workflow.command-z0e0fk0z.js} +12 -12
- package/{workspace.command-h9wwcjpt.js → workspace.command-3q1d6kk4.js} +21 -21
- package/templates/libRoot/base/baseLogic.ts +0 -14
- package/templates/libRoot/base/index.ts +0 -8
|
@@ -7,9 +7,12 @@ import {
|
|
|
7
7
|
command,
|
|
8
8
|
runner,
|
|
9
9
|
script
|
|
10
|
-
} from "./index-
|
|
11
|
-
import
|
|
12
|
-
|
|
10
|
+
} from "./index-jkynkyfk.js";
|
|
11
|
+
import {
|
|
12
|
+
appRootAllowedFiles,
|
|
13
|
+
libFacetRootAllowedFiles
|
|
14
|
+
} from "./index-47qxw1tz.js";
|
|
15
|
+
import"./index-mxvakhsm.js";
|
|
13
16
|
import"./index-67546d0j.js";
|
|
14
17
|
import"./index-r24hmh0q.js";
|
|
15
18
|
|
|
@@ -18,7 +21,369 @@ import { createHash } from "crypto";
|
|
|
18
21
|
import { readdir, readFile, stat } from "fs/promises";
|
|
19
22
|
import path from "path";
|
|
20
23
|
import ignore from "ignore";
|
|
24
|
+
import ts2 from "typescript";
|
|
25
|
+
|
|
26
|
+
// pkgs/@akanjs/devkit/ssrScanner.ts
|
|
21
27
|
import ts from "typescript";
|
|
28
|
+
var STATIC_COMPONENT_MIN_MASS = 4;
|
|
29
|
+
var MIXED_COMPONENT_MIN_MASS = 10;
|
|
30
|
+
var MIXED_COMPONENT_MAX_TOUCHES = 2;
|
|
31
|
+
var MODULE_SERVER_VIEW_MIN_CLIENT_MASS = 12;
|
|
32
|
+
|
|
33
|
+
class SsrScanner {
|
|
34
|
+
static #serverSafeCalls = new Set(["usePage", "getSelf", "useServer"]);
|
|
35
|
+
static #clientGlobals = new Set([
|
|
36
|
+
"window",
|
|
37
|
+
"document",
|
|
38
|
+
"navigator",
|
|
39
|
+
"localStorage",
|
|
40
|
+
"sessionStorage",
|
|
41
|
+
"location",
|
|
42
|
+
"history",
|
|
43
|
+
"screen",
|
|
44
|
+
"matchMedia",
|
|
45
|
+
"IntersectionObserver",
|
|
46
|
+
"ResizeObserver",
|
|
47
|
+
"MutationObserver",
|
|
48
|
+
"requestAnimationFrame",
|
|
49
|
+
"WebSocket"
|
|
50
|
+
]);
|
|
51
|
+
static #clientRuntimeImports = new Set(["st", "fetch"]);
|
|
52
|
+
scan(sourceFiles) {
|
|
53
|
+
const componentFiles = sourceFiles.filter((sourceFile) => this.#isBalancedFile(sourceFile.file));
|
|
54
|
+
return {
|
|
55
|
+
warnings: [
|
|
56
|
+
...componentFiles.flatMap((sourceFile) => this.#scanFile(sourceFile)),
|
|
57
|
+
...this.#scanModules(componentFiles)
|
|
58
|
+
],
|
|
59
|
+
balance: this.#measureBalance(componentFiles)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
#scanFile(sourceFile) {
|
|
63
|
+
if (!this.#hasUseClient(sourceFile.sourceFile))
|
|
64
|
+
return [];
|
|
65
|
+
const vendorNames = this.#getVendorNames(sourceFile.sourceFile);
|
|
66
|
+
const hasVendorImport = this.#hasVendorImport(sourceFile.sourceFile);
|
|
67
|
+
const importsClientRuntime = this.#importsClientRuntime(sourceFile.sourceFile);
|
|
68
|
+
const components = this.#getComponents(sourceFile, vendorNames);
|
|
69
|
+
const warnings = [];
|
|
70
|
+
if (!hasVendorImport && !importsClientRuntime && this.#getTouches(sourceFile.sourceFile, sourceFile.sourceFile).length === 0 && !this.#isConventionClientFile(sourceFile.file)) {
|
|
71
|
+
warnings.push({
|
|
72
|
+
rule: "akan.ssr.unnecessary-use-client",
|
|
73
|
+
scope: "ssr",
|
|
74
|
+
severity: "warning",
|
|
75
|
+
file: sourceFile.file,
|
|
76
|
+
line: 1,
|
|
77
|
+
message: `"use client" is declared but the file uses no client-only capability (hook, event handler, store, or browser API).`
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
for (const component of components) {
|
|
81
|
+
if (component.vendorTags)
|
|
82
|
+
continue;
|
|
83
|
+
if (component.touches.length === 0 && component.mass >= STATIC_COMPONENT_MIN_MASS) {
|
|
84
|
+
warnings.push({
|
|
85
|
+
rule: "akan.ssr.client-static-component",
|
|
86
|
+
scope: "ssr",
|
|
87
|
+
severity: "warning",
|
|
88
|
+
file: sourceFile.file,
|
|
89
|
+
line: component.line,
|
|
90
|
+
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.`
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (component.touches.length >= 1 && component.touches.length <= MIXED_COMPONENT_MAX_TOUCHES && component.mass >= MIXED_COMPONENT_MIN_MASS) {
|
|
95
|
+
warnings.push({
|
|
96
|
+
rule: "akan.ssr.client-static-markup",
|
|
97
|
+
scope: "ssr",
|
|
98
|
+
severity: "warning",
|
|
99
|
+
file: sourceFile.file,
|
|
100
|
+
line: component.line,
|
|
101
|
+
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.`
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
warnings.push(...this.#getMountLoadWarnings(sourceFile));
|
|
106
|
+
warnings.push(...this.#getTemplateStateWarnings(sourceFile));
|
|
107
|
+
return warnings;
|
|
108
|
+
}
|
|
109
|
+
#scanModules(sourceFiles) {
|
|
110
|
+
const modules = new Map;
|
|
111
|
+
for (const sourceFile of sourceFiles) {
|
|
112
|
+
const moduleDir = this.#getModuleDir(sourceFile.file);
|
|
113
|
+
if (!moduleDir)
|
|
114
|
+
continue;
|
|
115
|
+
const entry = modules.get(moduleDir) ?? { clientMass: 0, serverFiles: 0, line: sourceFile.file };
|
|
116
|
+
if (this.#hasUseClient(sourceFile.sourceFile))
|
|
117
|
+
entry.clientMass += this.#getMass(sourceFile.sourceFile);
|
|
118
|
+
else if (/\.(Unit|View)\.tsx$/.test(sourceFile.file))
|
|
119
|
+
entry.serverFiles += 1;
|
|
120
|
+
modules.set(moduleDir, entry);
|
|
121
|
+
}
|
|
122
|
+
return [...modules].filter(([, entry]) => entry.serverFiles === 0 && entry.clientMass >= MODULE_SERVER_VIEW_MIN_CLIENT_MASS).map(([moduleDir, entry]) => ({
|
|
123
|
+
rule: "akan.ssr.module-missing-server-view",
|
|
124
|
+
scope: "ssr",
|
|
125
|
+
severity: "warning",
|
|
126
|
+
file: entry.line,
|
|
127
|
+
message: `Module "${moduleDir}" renders ${entry.clientMass} JSX elements from client files only; it declares no Unit or View server component.`
|
|
128
|
+
}));
|
|
129
|
+
}
|
|
130
|
+
#getMountLoadWarnings(sourceFile) {
|
|
131
|
+
const warnings = [];
|
|
132
|
+
const visit = (node) => {
|
|
133
|
+
if (this.#isMountEffect(sourceFile.sourceFile, node)) {
|
|
134
|
+
for (const load of this.#getLoadCalls(sourceFile.sourceFile, node)) {
|
|
135
|
+
warnings.push({
|
|
136
|
+
rule: "akan.ssr.client-mount-load",
|
|
137
|
+
scope: "ssr",
|
|
138
|
+
severity: "warning",
|
|
139
|
+
file: sourceFile.file,
|
|
140
|
+
line: this.#getLine(sourceFile.sourceFile, load.node),
|
|
141
|
+
message: `Mount-only effect loads server data with ${load.callee}(). The route can fetch this before the first byte instead.`
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
ts.forEachChild(node, visit);
|
|
146
|
+
};
|
|
147
|
+
ts.forEachChild(sourceFile.sourceFile, visit);
|
|
148
|
+
return warnings;
|
|
149
|
+
}
|
|
150
|
+
#isMountEffect(sourceFile, node) {
|
|
151
|
+
if (!ts.isCallExpression(node))
|
|
152
|
+
return false;
|
|
153
|
+
const callee = node.expression.getText(sourceFile);
|
|
154
|
+
if (callee !== "useEffect" && callee !== "useLayoutEffect")
|
|
155
|
+
return false;
|
|
156
|
+
const deps = node.arguments[1];
|
|
157
|
+
return !!deps && ts.isArrayLiteralExpression(deps) && deps.elements.length === 0;
|
|
158
|
+
}
|
|
159
|
+
#getLoadCalls(sourceFile, node) {
|
|
160
|
+
const calls = [];
|
|
161
|
+
const visit = (child) => {
|
|
162
|
+
if (ts.isCallExpression(child)) {
|
|
163
|
+
const callee = child.expression.getText(sourceFile);
|
|
164
|
+
if (/^fetch\.[a-z]/.test(callee) || /^st\.do\.(init|get|view|load|list|count|insight)[A-Z]/.test(callee))
|
|
165
|
+
calls.push({ callee, node: child });
|
|
166
|
+
}
|
|
167
|
+
ts.forEachChild(child, visit);
|
|
168
|
+
};
|
|
169
|
+
ts.forEachChild(node, visit);
|
|
170
|
+
return calls;
|
|
171
|
+
}
|
|
172
|
+
#getTemplateStateWarnings(sourceFile) {
|
|
173
|
+
if (!sourceFile.file.endsWith(".Template.tsx"))
|
|
174
|
+
return [];
|
|
175
|
+
const warnings = [];
|
|
176
|
+
const visit = (node) => {
|
|
177
|
+
if (ts.isCallExpression(node) && node.expression.getText(sourceFile.sourceFile) === "useState") {
|
|
178
|
+
warnings.push({
|
|
179
|
+
rule: "akan.ssr.template-client-state",
|
|
180
|
+
scope: "ssr",
|
|
181
|
+
severity: "warning",
|
|
182
|
+
file: sourceFile.file,
|
|
183
|
+
line: this.#getLine(sourceFile.sourceFile, node),
|
|
184
|
+
message: "Template holds form state in useState. Templates are store-driven and carry no local state."
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
ts.forEachChild(node, visit);
|
|
188
|
+
};
|
|
189
|
+
ts.forEachChild(sourceFile.sourceFile, visit);
|
|
190
|
+
return warnings;
|
|
191
|
+
}
|
|
192
|
+
#measureBalance(sourceFiles) {
|
|
193
|
+
const scopes = new Map;
|
|
194
|
+
for (const sourceFile of sourceFiles) {
|
|
195
|
+
const segments = sourceFile.file.split("/");
|
|
196
|
+
const scope = `${segments[0]}/${segments[1]}`;
|
|
197
|
+
const entry = scopes.get(scope) ?? { serverMass: 0, clientMass: 0 };
|
|
198
|
+
const mass = this.#getMass(sourceFile.sourceFile);
|
|
199
|
+
if (this.#hasUseClient(sourceFile.sourceFile))
|
|
200
|
+
entry.clientMass += mass;
|
|
201
|
+
else
|
|
202
|
+
entry.serverMass += mass;
|
|
203
|
+
scopes.set(scope, entry);
|
|
204
|
+
}
|
|
205
|
+
const entries = [...scopes].map(([scope, mass]) => ({ scope, ...mass, serverShare: getShare(mass.serverMass, mass.clientMass) })).sort((a, b) => a.scope.localeCompare(b.scope));
|
|
206
|
+
if (entries.length < 2)
|
|
207
|
+
return entries;
|
|
208
|
+
const serverMass = entries.reduce((sum, entry) => sum + entry.serverMass, 0);
|
|
209
|
+
const clientMass = entries.reduce((sum, entry) => sum + entry.clientMass, 0);
|
|
210
|
+
return [...entries, { scope: "workspace", serverMass, clientMass, serverShare: getShare(serverMass, clientMass) }];
|
|
211
|
+
}
|
|
212
|
+
#getComponents(sourceFile, vendorNames) {
|
|
213
|
+
const components = [];
|
|
214
|
+
for (const statement of sourceFile.sourceFile.statements) {
|
|
215
|
+
for (const { name, node } of this.#getComponentNodes(statement)) {
|
|
216
|
+
if (!/^[A-Z]/.test(name))
|
|
217
|
+
continue;
|
|
218
|
+
components.push({
|
|
219
|
+
name,
|
|
220
|
+
line: this.#getLine(sourceFile.sourceFile, node),
|
|
221
|
+
mass: this.#getMass(node),
|
|
222
|
+
touches: this.#getTouches(sourceFile.sourceFile, node),
|
|
223
|
+
vendorTags: [...this.#getTagNames(sourceFile.sourceFile, node)].some((tag) => vendorNames.has(tag))
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return components;
|
|
228
|
+
}
|
|
229
|
+
#getComponentNodes(statement) {
|
|
230
|
+
if (ts.isFunctionDeclaration(statement) && statement.body)
|
|
231
|
+
return [{ name: statement.name?.text ?? "default", node: statement.body }];
|
|
232
|
+
if (!ts.isVariableStatement(statement))
|
|
233
|
+
return [];
|
|
234
|
+
return statement.declarationList.declarations.flatMap((declaration) => {
|
|
235
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer)
|
|
236
|
+
return [];
|
|
237
|
+
if (!ts.isArrowFunction(declaration.initializer) && !ts.isFunctionExpression(declaration.initializer))
|
|
238
|
+
return [];
|
|
239
|
+
return [{ name: declaration.name.text, node: declaration.initializer }];
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
#getTouches(sourceFile, node) {
|
|
243
|
+
const touches = [];
|
|
244
|
+
const visit = (child) => {
|
|
245
|
+
if (ts.isCallExpression(child)) {
|
|
246
|
+
const callee = child.expression.getText(sourceFile);
|
|
247
|
+
const bareName = callee.split(".").pop() ?? callee;
|
|
248
|
+
if (callee === "createContext" || callee === "lazy")
|
|
249
|
+
touches.push(callee);
|
|
250
|
+
else if (/^use[A-Z]/.test(bareName) && !SsrScanner.#serverSafeCalls.has(bareName))
|
|
251
|
+
touches.push(bareName);
|
|
252
|
+
}
|
|
253
|
+
if (ts.isJsxAttribute(child) && /^on[A-Z]/.test(child.name.getText(sourceFile)))
|
|
254
|
+
touches.push(child.name.getText(sourceFile));
|
|
255
|
+
if (ts.isPropertyAccessExpression(child)) {
|
|
256
|
+
const root = getAccessRoot(child);
|
|
257
|
+
if (root === "st")
|
|
258
|
+
touches.push("st");
|
|
259
|
+
else if (SsrScanner.#clientGlobals.has(root))
|
|
260
|
+
touches.push(root);
|
|
261
|
+
}
|
|
262
|
+
ts.forEachChild(child, visit);
|
|
263
|
+
};
|
|
264
|
+
ts.forEachChild(node, visit);
|
|
265
|
+
return touches;
|
|
266
|
+
}
|
|
267
|
+
#getTagNames(sourceFile, node) {
|
|
268
|
+
const tags = new Set;
|
|
269
|
+
const visit = (child) => {
|
|
270
|
+
if (ts.isJsxOpeningElement(child) || ts.isJsxSelfClosingElement(child))
|
|
271
|
+
tags.add(child.tagName.getText(sourceFile).split(".")[0]);
|
|
272
|
+
ts.forEachChild(child, visit);
|
|
273
|
+
};
|
|
274
|
+
ts.forEachChild(node, visit);
|
|
275
|
+
return tags;
|
|
276
|
+
}
|
|
277
|
+
#getMass(node) {
|
|
278
|
+
let mass = 0;
|
|
279
|
+
const visit = (child) => {
|
|
280
|
+
if (ts.isJsxOpeningElement(child) || ts.isJsxSelfClosingElement(child))
|
|
281
|
+
mass += 1;
|
|
282
|
+
ts.forEachChild(child, visit);
|
|
283
|
+
};
|
|
284
|
+
ts.forEachChild(node, visit);
|
|
285
|
+
return mass;
|
|
286
|
+
}
|
|
287
|
+
#hasUseClient(sourceFile) {
|
|
288
|
+
const first = sourceFile.statements[0];
|
|
289
|
+
if (!first || !ts.isExpressionStatement(first) || !ts.isStringLiteral(first.expression))
|
|
290
|
+
return false;
|
|
291
|
+
return first.expression.text === "use client";
|
|
292
|
+
}
|
|
293
|
+
#hasVendorImport(sourceFile) {
|
|
294
|
+
return sourceFile.statements.some((statement) => ts.isImportDeclaration(statement) && isVendorSpecifier(getSpecifier(statement)));
|
|
295
|
+
}
|
|
296
|
+
#getVendorNames(sourceFile) {
|
|
297
|
+
const names = new Set;
|
|
298
|
+
for (const statement of sourceFile.statements) {
|
|
299
|
+
if (!ts.isImportDeclaration(statement) || !isVendorSpecifier(getSpecifier(statement)))
|
|
300
|
+
continue;
|
|
301
|
+
const clause = statement.importClause;
|
|
302
|
+
if (clause?.name)
|
|
303
|
+
names.add(clause.name.text);
|
|
304
|
+
if (clause?.namedBindings && ts.isNamespaceImport(clause.namedBindings))
|
|
305
|
+
names.add(clause.namedBindings.name.text);
|
|
306
|
+
if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings))
|
|
307
|
+
for (const element of clause.namedBindings.elements)
|
|
308
|
+
names.add(element.name.text);
|
|
309
|
+
}
|
|
310
|
+
return names;
|
|
311
|
+
}
|
|
312
|
+
#importsClientRuntime(sourceFile) {
|
|
313
|
+
for (const statement of sourceFile.statements) {
|
|
314
|
+
if (!ts.isImportDeclaration(statement))
|
|
315
|
+
continue;
|
|
316
|
+
const bindings = statement.importClause?.namedBindings;
|
|
317
|
+
if (!bindings || !ts.isNamedImports(bindings))
|
|
318
|
+
continue;
|
|
319
|
+
if (bindings.elements.some((element) => SsrScanner.#clientRuntimeImports.has(element.name.text)))
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
#isConventionClientFile(file) {
|
|
325
|
+
if (file.endsWith("/index_.tsx"))
|
|
326
|
+
return true;
|
|
327
|
+
return /\.(Zone|Template|Util)\.tsx$/.test(file) && this.#getModuleDir(file) !== null;
|
|
328
|
+
}
|
|
329
|
+
#isBalancedFile(file) {
|
|
330
|
+
if (!file.endsWith(".tsx") || file.endsWith(".test.tsx") || file.endsWith(".spec.tsx"))
|
|
331
|
+
return false;
|
|
332
|
+
const segments = file.split("/");
|
|
333
|
+
if (segments[0] !== "apps" && segments[0] !== "libs")
|
|
334
|
+
return false;
|
|
335
|
+
return segments[2] === "ui" || segments[2] === "lib";
|
|
336
|
+
}
|
|
337
|
+
#getModuleDir(file) {
|
|
338
|
+
const segments = file.split("/");
|
|
339
|
+
const libIndex = segments.indexOf("lib");
|
|
340
|
+
if (libIndex < 1 || segments.length <= libIndex + 2)
|
|
341
|
+
return null;
|
|
342
|
+
const moduleName = segments[libIndex + 1];
|
|
343
|
+
if (moduleName.startsWith("_"))
|
|
344
|
+
return null;
|
|
345
|
+
return segments.slice(0, libIndex + 2).join("/");
|
|
346
|
+
}
|
|
347
|
+
#getLine(sourceFile, node) {
|
|
348
|
+
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
var SSR_SERVER_SHARE_TARGET = 0.5;
|
|
352
|
+
function formatSsrBalance(balance) {
|
|
353
|
+
if (balance.length === 0)
|
|
354
|
+
return ["No component files found."];
|
|
355
|
+
return balance.map((entry) => {
|
|
356
|
+
const total = entry.serverMass + entry.clientMass;
|
|
357
|
+
const share = `${Math.round(entry.serverShare * 100)}% server`;
|
|
358
|
+
const counts = `${entry.serverMass} of ${total} JSX elements, ${entry.clientMass} client`;
|
|
359
|
+
const flag = entry.serverShare < SSR_SERVER_SHARE_TARGET ? ` <- below the ${Math.round(SSR_SERVER_SHARE_TARGET * 100)}% target` : "";
|
|
360
|
+
return ` ${entry.scope}: ${share} (${counts})${flag}`;
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
function getShare(serverMass, clientMass) {
|
|
364
|
+
const total = serverMass + clientMass;
|
|
365
|
+
return total === 0 ? 1 : serverMass / total;
|
|
366
|
+
}
|
|
367
|
+
function getSpecifier(statement) {
|
|
368
|
+
return ts.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : "";
|
|
369
|
+
}
|
|
370
|
+
function isVendorSpecifier(specifier) {
|
|
371
|
+
if (specifier === "" || specifier.startsWith(".") || specifier.startsWith("/"))
|
|
372
|
+
return false;
|
|
373
|
+
if (specifier === "react" || specifier === "react-dom" || specifier.startsWith("react/"))
|
|
374
|
+
return false;
|
|
375
|
+
if (specifier.startsWith("node:"))
|
|
376
|
+
return false;
|
|
377
|
+
return !/^(akanjs|@akanjs|@libs|@apps|@contract)(\/|$)/.test(specifier);
|
|
378
|
+
}
|
|
379
|
+
function getAccessRoot(node) {
|
|
380
|
+
let current = node;
|
|
381
|
+
while (ts.isPropertyAccessExpression(current))
|
|
382
|
+
current = current.expression;
|
|
383
|
+
return ts.isIdentifier(current) ? current.text : "";
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// pkgs/@akanjs/devkit/qualityScanner.ts
|
|
22
387
|
var MAX_FILE_LINES = 2000;
|
|
23
388
|
var PLACEHOLDER_EXPORT_NAMES = new Set([
|
|
24
389
|
"aa",
|
|
@@ -42,27 +407,6 @@ var SUGGESTED_RULES = [
|
|
|
42
407
|
"Move shared app utilities to apps/*/common instead of creating apps/*/base.",
|
|
43
408
|
"Avoid large mixed-purpose class files; class export files should import helpers from neighboring utility files instead of declaring them inline."
|
|
44
409
|
];
|
|
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
410
|
var CONVENTION_SUFFIXES = [
|
|
67
411
|
".constant.ts",
|
|
68
412
|
".dictionary.ts",
|
|
@@ -101,7 +445,13 @@ var RULE_FIXES = {
|
|
|
101
445
|
"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
446
|
"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
447
|
"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."
|
|
448
|
+
"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.",
|
|
449
|
+
"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.',
|
|
450
|
+
"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.",
|
|
451
|
+
"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.",
|
|
452
|
+
"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.",
|
|
453
|
+
"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.",
|
|
454
|
+
"akan.ssr.template-client-state": "Bind the field to the store instead: `value={xForm.field}` with `onChange={st.do.setFieldOnX}`."
|
|
105
455
|
};
|
|
106
456
|
function getRuleFix(rule) {
|
|
107
457
|
if (rule.startsWith("akan.convention"))
|
|
@@ -114,18 +464,21 @@ class AkanQualityScanner {
|
|
|
114
464
|
const targetFiles = await this.#collectTargetFiles(workspaceRoot);
|
|
115
465
|
const sourceFiles = await Promise.all(targetFiles.filter((file) => !AbstractDoc.isAbstractPath(file)).map((file) => this.#readSourceFile(workspaceRoot, file)));
|
|
116
466
|
const abstractFiles = await Promise.all(targetFiles.filter((file) => AbstractDoc.isAbstractPath(file)).map((file) => this.#readTextFile(workspaceRoot, file)));
|
|
467
|
+
const ssr = new SsrScanner().scan(sourceFiles);
|
|
117
468
|
const warnings = [
|
|
118
469
|
...this.#scanGlobalQuality(sourceFiles),
|
|
119
470
|
...sourceFiles.flatMap((sourceFile) => this.#scanSingleFileQuality(sourceFile)),
|
|
120
471
|
...sourceFiles.flatMap((sourceFile) => this.#scanComponentQuality(sourceFile)),
|
|
121
472
|
...sourceFiles.flatMap((sourceFile) => this.#scanConventionQuality(sourceFile)),
|
|
122
473
|
...sourceFiles.flatMap((sourceFile) => this.#scanLayoutQuality(sourceFile)),
|
|
123
|
-
...abstractFiles.flatMap((abstractFile) => this.#scanAbstractQuality(abstractFile))
|
|
474
|
+
...abstractFiles.flatMap((abstractFile) => this.#scanAbstractQuality(abstractFile)),
|
|
475
|
+
...ssr.warnings
|
|
124
476
|
];
|
|
125
477
|
return {
|
|
126
478
|
workspaceRoot,
|
|
127
479
|
scannedFiles: sourceFiles.length + abstractFiles.length,
|
|
128
480
|
warnings: warnings.map((warning) => ({ ...warning, fix: warning.fix ?? getRuleFix(warning.rule) })).sort(compareWarnings),
|
|
481
|
+
ssrBalance: ssr.balance,
|
|
129
482
|
suggestedRules: SUGGESTED_RULES
|
|
130
483
|
};
|
|
131
484
|
}
|
|
@@ -171,7 +524,7 @@ class AkanQualityScanner {
|
|
|
171
524
|
file,
|
|
172
525
|
absolutePath,
|
|
173
526
|
content,
|
|
174
|
-
sourceFile:
|
|
527
|
+
sourceFile: ts2.createSourceFile(file, content, ts2.ScriptTarget.Latest, true, getScriptKind(file))
|
|
175
528
|
};
|
|
176
529
|
}
|
|
177
530
|
async#readTextFile(workspaceRoot, file) {
|
|
@@ -332,7 +685,7 @@ class AkanQualityScanner {
|
|
|
332
685
|
#scanLayoutQuality(sourceFile) {
|
|
333
686
|
const segments = sourceFile.file.split("/");
|
|
334
687
|
const warnings = [];
|
|
335
|
-
if (segments[0] === "apps" && segments.length === 3 && !
|
|
688
|
+
if (segments[0] === "apps" && segments.length === 3 && !appRootAllowedFiles.has(segments[2])) {
|
|
336
689
|
warnings.push({
|
|
337
690
|
rule: "akan.layout.app-root-file",
|
|
338
691
|
scope: "layout",
|
|
@@ -342,7 +695,7 @@ class AkanQualityScanner {
|
|
|
342
695
|
});
|
|
343
696
|
}
|
|
344
697
|
const libRootFile = getLibRootFile(sourceFile.file);
|
|
345
|
-
if (libRootFile && !
|
|
698
|
+
if (libRootFile && !libFacetRootAllowedFiles.has(libRootFile)) {
|
|
346
699
|
warnings.push({
|
|
347
700
|
rule: "akan.layout.lib-root-file",
|
|
348
701
|
scope: "layout",
|
|
@@ -368,6 +721,10 @@ function formatQualityScanResult(result) {
|
|
|
368
721
|
"",
|
|
369
722
|
...formatQualityWarnings(result.warnings),
|
|
370
723
|
"",
|
|
724
|
+
"SSR balance (component files, JSX elements rendered per side):",
|
|
725
|
+
"",
|
|
726
|
+
...formatSsrBalance(result.ssrBalance),
|
|
727
|
+
"",
|
|
371
728
|
"Suggested quality rules:",
|
|
372
729
|
"",
|
|
373
730
|
...result.suggestedRules.map((rule) => ` - ${rule}`)
|
|
@@ -375,6 +732,24 @@ function formatQualityScanResult(result) {
|
|
|
375
732
|
return sections.join(`
|
|
376
733
|
`);
|
|
377
734
|
}
|
|
735
|
+
function formatSsrScanResult(result) {
|
|
736
|
+
const sections = [
|
|
737
|
+
"Akan SSR Balance Scan",
|
|
738
|
+
`workspace: ${result.workspaceRoot}`,
|
|
739
|
+
`scanned files: ${result.scannedFiles}`,
|
|
740
|
+
`ssr warnings: ${result.warnings.length}`,
|
|
741
|
+
"",
|
|
742
|
+
"Server render share (component files, JSX elements rendered per side):",
|
|
743
|
+
"",
|
|
744
|
+
...formatSsrBalance(result.ssrBalance),
|
|
745
|
+
"",
|
|
746
|
+
"Warnings:",
|
|
747
|
+
"",
|
|
748
|
+
...formatQualityWarnings(result.warnings)
|
|
749
|
+
];
|
|
750
|
+
return sections.join(`
|
|
751
|
+
`);
|
|
752
|
+
}
|
|
378
753
|
function formatQualityWarnings(warnings) {
|
|
379
754
|
if (warnings.length === 0)
|
|
380
755
|
return ["No warnings found."];
|
|
@@ -396,7 +771,7 @@ function getExportedFunctionLikes(sourceFile) {
|
|
|
396
771
|
const declarations = [];
|
|
397
772
|
const nameExempt = isPageRouteFile(sourceFile.file) || isUiComponentFile(sourceFile.file);
|
|
398
773
|
for (const statement of sourceFile.sourceFile.statements) {
|
|
399
|
-
if (
|
|
774
|
+
if (ts2.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
|
|
400
775
|
declarations.push({
|
|
401
776
|
name: statement.name.text,
|
|
402
777
|
kind: "function",
|
|
@@ -406,7 +781,7 @@ function getExportedFunctionLikes(sourceFile) {
|
|
|
406
781
|
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false)
|
|
407
782
|
});
|
|
408
783
|
}
|
|
409
|
-
if (
|
|
784
|
+
if (ts2.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
410
785
|
declarations.push({
|
|
411
786
|
name: statement.name.text,
|
|
412
787
|
kind: "class",
|
|
@@ -416,9 +791,9 @@ function getExportedFunctionLikes(sourceFile) {
|
|
|
416
791
|
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, isEnumClassStatement(sourceFile.sourceFile, statement))
|
|
417
792
|
});
|
|
418
793
|
}
|
|
419
|
-
if (
|
|
794
|
+
if (ts2.isVariableStatement(statement) && isExported(statement)) {
|
|
420
795
|
for (const declaration of statement.declarationList.declarations) {
|
|
421
|
-
if (!
|
|
796
|
+
if (!ts2.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
|
|
422
797
|
continue;
|
|
423
798
|
declarations.push({
|
|
424
799
|
name: declaration.name.text,
|
|
@@ -457,14 +832,14 @@ function isInLibModule(file) {
|
|
|
457
832
|
return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
|
|
458
833
|
}
|
|
459
834
|
function isEnumClassStatement(sourceFile, statement) {
|
|
460
|
-
if (!
|
|
835
|
+
if (!ts2.isClassDeclaration(statement))
|
|
461
836
|
return false;
|
|
462
|
-
const heritageClause = statement.heritageClauses?.find((clause) => clause.token ===
|
|
837
|
+
const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts2.SyntaxKind.ExtendsKeyword);
|
|
463
838
|
const expression = heritageClause?.types[0]?.expression;
|
|
464
839
|
return !!expression && expression.getText(sourceFile).startsWith("enumOf(");
|
|
465
840
|
}
|
|
466
841
|
function getExportedClassNames(sourceFile) {
|
|
467
|
-
return sourceFile.statements.filter((statement) =>
|
|
842
|
+
return sourceFile.statements.filter((statement) => ts2.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
|
|
468
843
|
}
|
|
469
844
|
function isComponentDeclarationFile(file) {
|
|
470
845
|
if (!file.endsWith(".tsx"))
|
|
@@ -480,7 +855,7 @@ function isComponentDeclarationFile(file) {
|
|
|
480
855
|
function getComponentFileDeclarations(sourceFile) {
|
|
481
856
|
const reExportedNames = new Set;
|
|
482
857
|
for (const statement of sourceFile.statements) {
|
|
483
|
-
if (
|
|
858
|
+
if (ts2.isExportDeclaration(statement) && statement.exportClause && ts2.isNamedExports(statement.exportClause)) {
|
|
484
859
|
for (const element of statement.exportClause.elements)
|
|
485
860
|
reExportedNames.add((element.propertyName ?? element.name).text);
|
|
486
861
|
}
|
|
@@ -490,19 +865,19 @@ function getComponentFileDeclarations(sourceFile) {
|
|
|
490
865
|
const isDefaultExport = isDefaultExportStatement(statement);
|
|
491
866
|
const inlineExported = isExported(statement);
|
|
492
867
|
const add = (name, kind, line) => declarations.push({ name, kind, line, exported: inlineExported || reExportedNames.has(name), isDefaultExport });
|
|
493
|
-
if (
|
|
868
|
+
if (ts2.isInterfaceDeclaration(statement))
|
|
494
869
|
add(statement.name.text, "interface", getLine(sourceFile, statement));
|
|
495
|
-
else if (
|
|
870
|
+
else if (ts2.isTypeAliasDeclaration(statement))
|
|
496
871
|
add(statement.name.text, "type", getLine(sourceFile, statement));
|
|
497
|
-
else if (
|
|
872
|
+
else if (ts2.isEnumDeclaration(statement))
|
|
498
873
|
add(statement.name.text, "enum", getLine(sourceFile, statement));
|
|
499
|
-
else if (
|
|
874
|
+
else if (ts2.isFunctionDeclaration(statement) && statement.name)
|
|
500
875
|
add(statement.name.text, "function", getLine(sourceFile, statement));
|
|
501
|
-
else if (
|
|
876
|
+
else if (ts2.isClassDeclaration(statement) && statement.name)
|
|
502
877
|
add(statement.name.text, "class", getLine(sourceFile, statement));
|
|
503
|
-
else if (
|
|
878
|
+
else if (ts2.isVariableStatement(statement)) {
|
|
504
879
|
for (const declaration of statement.declarationList.declarations) {
|
|
505
|
-
if (!
|
|
880
|
+
if (!ts2.isIdentifier(declaration.name))
|
|
506
881
|
continue;
|
|
507
882
|
const kind = isFunctionLikeInitializer(declaration.initializer) ? "function" : "variable";
|
|
508
883
|
add(declaration.name.text, kind, getLine(sourceFile, declaration));
|
|
@@ -514,15 +889,15 @@ function getComponentFileDeclarations(sourceFile) {
|
|
|
514
889
|
function getCompoundComponentNames(sourceFile) {
|
|
515
890
|
const names = new Set;
|
|
516
891
|
for (const statement of sourceFile.statements) {
|
|
517
|
-
if (!
|
|
892
|
+
if (!ts2.isExpressionStatement(statement))
|
|
518
893
|
continue;
|
|
519
894
|
const { expression } = statement;
|
|
520
|
-
if (!
|
|
895
|
+
if (!ts2.isBinaryExpression(expression) || expression.operatorToken.kind !== ts2.SyntaxKind.EqualsToken)
|
|
521
896
|
continue;
|
|
522
|
-
if (!
|
|
897
|
+
if (!ts2.isPropertyAccessExpression(expression.left) || !isPascalCaseName(expression.left.name.text))
|
|
523
898
|
continue;
|
|
524
899
|
names.add(expression.left.name.text);
|
|
525
|
-
if (
|
|
900
|
+
if (ts2.isIdentifier(expression.right) && isPascalCaseName(expression.right.text))
|
|
526
901
|
names.add(expression.right.text);
|
|
527
902
|
}
|
|
528
903
|
return names;
|
|
@@ -542,9 +917,9 @@ function isPascalCaseName(name) {
|
|
|
542
917
|
return /^[A-Z]/.test(name) && !/^[A-Z0-9_]+$/.test(name);
|
|
543
918
|
}
|
|
544
919
|
function isDefaultExportStatement(statement) {
|
|
545
|
-
if (
|
|
920
|
+
if (ts2.isExportAssignment(statement))
|
|
546
921
|
return !statement.isExportEquals;
|
|
547
|
-
return !!(
|
|
922
|
+
return !!(ts2.getCombinedModifierFlags(statement) & ts2.ModifierFlags.Default);
|
|
548
923
|
}
|
|
549
924
|
function getPlaceholderExportWarnings(sourceFile) {
|
|
550
925
|
if (!sourceFile.file.endsWith("/index.ts") && !sourceFile.file.endsWith("/index.tsx"))
|
|
@@ -574,7 +949,7 @@ function getDictionaryTextWarnings(sourceFile) {
|
|
|
574
949
|
function getGlobalMutationWarnings(sourceFile) {
|
|
575
950
|
const warnings = [];
|
|
576
951
|
for (const statement of sourceFile.sourceFile.statements) {
|
|
577
|
-
if (
|
|
952
|
+
if (ts2.isModuleDeclaration(statement) && statement.name.getText(sourceFile.sourceFile) === "global") {
|
|
578
953
|
warnings.push({
|
|
579
954
|
rule: "akan.file.global-declaration",
|
|
580
955
|
scope: "file",
|
|
@@ -584,7 +959,7 @@ function getGlobalMutationWarnings(sourceFile) {
|
|
|
584
959
|
message: "Global declarations require an explicit low-level integration allowlist."
|
|
585
960
|
});
|
|
586
961
|
}
|
|
587
|
-
if (
|
|
962
|
+
if (ts2.isInterfaceDeclaration(statement) && statement.name.text === "Window") {
|
|
588
963
|
warnings.push({
|
|
589
964
|
rule: "akan.file.window-augmentation",
|
|
590
965
|
scope: "file",
|
|
@@ -594,7 +969,7 @@ function getGlobalMutationWarnings(sourceFile) {
|
|
|
594
969
|
message: "Window augmentation should be isolated to approved browser integration files."
|
|
595
970
|
});
|
|
596
971
|
}
|
|
597
|
-
if (
|
|
972
|
+
if (ts2.isExpressionStatement(statement) && statement.expression.getText(sourceFile.sourceFile).includes(".prototype.")) {
|
|
598
973
|
warnings.push({
|
|
599
974
|
rule: "akan.file.prototype-mutation",
|
|
600
975
|
scope: "file",
|
|
@@ -612,23 +987,23 @@ function getTopLevelDeclarations(sourceFile) {
|
|
|
612
987
|
}
|
|
613
988
|
function getTopLevelDeclaration(sourceFile, statement) {
|
|
614
989
|
const line = getLine(sourceFile, statement);
|
|
615
|
-
if (
|
|
990
|
+
if (ts2.isClassDeclaration(statement) && statement.name) {
|
|
616
991
|
return [{ name: statement.name.text, kind: "class", line, exported: isExported(statement), node: statement }];
|
|
617
992
|
}
|
|
618
|
-
if (
|
|
993
|
+
if (ts2.isFunctionDeclaration(statement) && statement.name) {
|
|
619
994
|
return [{ name: statement.name.text, kind: "function", line, exported: isExported(statement), node: statement }];
|
|
620
995
|
}
|
|
621
|
-
if (
|
|
996
|
+
if (ts2.isInterfaceDeclaration(statement)) {
|
|
622
997
|
return [{ name: statement.name.text, kind: "interface", line, exported: isExported(statement), node: statement }];
|
|
623
998
|
}
|
|
624
|
-
if (
|
|
999
|
+
if (ts2.isTypeAliasDeclaration(statement)) {
|
|
625
1000
|
return [{ name: statement.name.text, kind: "type", line, exported: isExported(statement), node: statement }];
|
|
626
1001
|
}
|
|
627
|
-
if (
|
|
1002
|
+
if (ts2.isEnumDeclaration(statement)) {
|
|
628
1003
|
return [{ name: statement.name.text, kind: "enum", line, exported: isExported(statement), node: statement }];
|
|
629
1004
|
}
|
|
630
|
-
if (
|
|
631
|
-
return statement.declarationList.declarations.filter((declaration) =>
|
|
1005
|
+
if (ts2.isVariableStatement(statement)) {
|
|
1006
|
+
return statement.declarationList.declarations.filter((declaration) => ts2.isIdentifier(declaration.name)).map((declaration) => ({
|
|
632
1007
|
name: declaration.name.text,
|
|
633
1008
|
kind: "variable",
|
|
634
1009
|
line: getLine(sourceFile, declaration),
|
|
@@ -636,7 +1011,7 @@ function getTopLevelDeclaration(sourceFile, statement) {
|
|
|
636
1011
|
node: statement
|
|
637
1012
|
}));
|
|
638
1013
|
}
|
|
639
|
-
if (
|
|
1014
|
+
if (ts2.isExportDeclaration(statement)) {
|
|
640
1015
|
return [{ name: "export declaration", kind: "export", line, exported: true, node: statement }];
|
|
641
1016
|
}
|
|
642
1017
|
return [];
|
|
@@ -661,9 +1036,9 @@ function isAllowedConstantDeclaration(modelName, declaration) {
|
|
|
661
1036
|
return false;
|
|
662
1037
|
if ([`${modelName}Input`, `${modelName}Object`, modelName, `Light${modelName}`, `${modelName}Insight`].includes(declaration.name))
|
|
663
1038
|
return true;
|
|
664
|
-
if (!
|
|
1039
|
+
if (!ts2.isClassDeclaration(declaration.node))
|
|
665
1040
|
return false;
|
|
666
|
-
const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token ===
|
|
1041
|
+
const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token === ts2.SyntaxKind.ExtendsKeyword);
|
|
667
1042
|
const expression = heritageClause?.types[0]?.expression;
|
|
668
1043
|
return !!expression && expression.getText().startsWith("enumOf(");
|
|
669
1044
|
}
|
|
@@ -729,13 +1104,13 @@ function getModuleInfo(file) {
|
|
|
729
1104
|
return { moduleName, fileName, kind: "database" };
|
|
730
1105
|
}
|
|
731
1106
|
function isExportedConst(declaration) {
|
|
732
|
-
return declaration.exported &&
|
|
1107
|
+
return declaration.exported && ts2.isVariableStatement(declaration.node) && (declaration.node.declarationList.flags & ts2.NodeFlags.Const) !== 0;
|
|
733
1108
|
}
|
|
734
1109
|
function isExported(node) {
|
|
735
|
-
return !!
|
|
1110
|
+
return !!ts2.getCombinedModifierFlags(node) && !!(ts2.getCombinedModifierFlags(node) & ts2.ModifierFlags.Export);
|
|
736
1111
|
}
|
|
737
1112
|
function isFunctionLikeInitializer(node) {
|
|
738
|
-
return !!node && (
|
|
1113
|
+
return !!node && (ts2.isArrowFunction(node) || ts2.isFunctionExpression(node));
|
|
739
1114
|
}
|
|
740
1115
|
function getBodyFingerprint(sourceFile, node) {
|
|
741
1116
|
if (!node)
|
|
@@ -757,7 +1132,7 @@ async function isDirectory(absolutePath) {
|
|
|
757
1132
|
}
|
|
758
1133
|
}
|
|
759
1134
|
function getScriptKind(file) {
|
|
760
|
-
return file.endsWith(".tsx") ?
|
|
1135
|
+
return file.endsWith(".tsx") ? ts2.ScriptKind.TSX : ts2.ScriptKind.TS;
|
|
761
1136
|
}
|
|
762
1137
|
function getLine(sourceFile, node) {
|
|
763
1138
|
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
@@ -810,6 +1185,13 @@ class QualityScript extends script("quality", [QualityRunner]) {
|
|
|
810
1185
|
spinner.succeed(`Quality scan completed (${result.scannedFiles} files, ${result.warnings.length} warnings)`);
|
|
811
1186
|
Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatQualityScanResult(result));
|
|
812
1187
|
}
|
|
1188
|
+
async ssr(workspace, format = "text") {
|
|
1189
|
+
const spinner = workspace.spinning("Measuring Akan server/client render balance...");
|
|
1190
|
+
const scanned = await this.qualityRunner.scan(workspace);
|
|
1191
|
+
const result = { ...scanned, warnings: scanned.warnings.filter((warning) => warning.scope === "ssr") };
|
|
1192
|
+
spinner.succeed(`SSR scan completed (${result.scannedFiles} files, ${result.warnings.length} warnings)`);
|
|
1193
|
+
Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSsrScanResult(result));
|
|
1194
|
+
}
|
|
813
1195
|
}
|
|
814
1196
|
|
|
815
1197
|
// pkgs/@akanjs/cli/quality/quality.command.ts
|
|
@@ -817,15 +1199,18 @@ class QualityCommand extends command("quality", [QualityScript], ({ public: targ
|
|
|
817
1199
|
quality: target({ desc: "Scan apps and libs for Akan code quality warnings" }).arg("action", String, {
|
|
818
1200
|
desc: "quality action",
|
|
819
1201
|
default: "scan",
|
|
820
|
-
enum: ["scan"]
|
|
1202
|
+
enum: ["scan", "ssr"]
|
|
821
1203
|
}).option("format", String, {
|
|
822
1204
|
desc: "output format",
|
|
823
1205
|
default: "text",
|
|
824
1206
|
enum: ["text", "json"]
|
|
825
1207
|
}).with(Workspace).exec(async function(action, format, workspace) {
|
|
826
|
-
if (action
|
|
827
|
-
|
|
828
|
-
|
|
1208
|
+
if (action === "scan")
|
|
1209
|
+
await this.qualityScript.scan(workspace, format);
|
|
1210
|
+
else if (action === "ssr")
|
|
1211
|
+
await this.qualityScript.ssr(workspace, format);
|
|
1212
|
+
else
|
|
1213
|
+
throw new Error(`Unknown quality action: ${action}. Use "scan" or "ssr".`);
|
|
829
1214
|
})
|
|
830
1215
|
})) {
|
|
831
1216
|
}
|