@akanjs/cli 3.0.0-alpha.0 → 3.0.0-alpha.1
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-yv00p0s0.js} +7 -7
- package/{application.command-3smsv7mq.js → application.command-0qsczf4k.js} +5 -5
- package/{applicationBuildRunner-aq7mb3z6.js → applicationBuildRunner-ct2pxyvw.js} +1 -1
- package/{applicationReleasePackager-yrrhbkv3.js → applicationReleasePackager-gg8ccbeh.js} +1 -1
- package/buildBatch.proc.js +2 -2
- package/{capacitorApp-q3mpmkkz.js → capacitorApp-4r9sm3qg.js} +3 -3
- package/{cloud.command-rk4e9vrt.js → cloud.command-dxfv18sa.js} +9 -9
- package/{context.command-ygzyb0xb.js → context.command-8d9e3k6b.js} +15 -15
- package/{guideline.command-1m1ctar3.js → guideline.command-3jbkxzh6.js} +5 -5
- package/guidelines/ssrRule/ssrRule.generate.json +31 -0
- package/guidelines/ssrRule/ssrRule.instruction.md +108 -0
- package/incrementalBuilder.proc.js +2 -2
- package/{index-3pgtw35d.js → index-0cj2zxbm.js} +1 -1
- package/{index-h0bngh79.js → index-1pvex4az.js} +11 -11
- package/{index-csqk9z5j.js → index-2q1f5j9k.js} +3 -3
- package/{index-cn24dcqn.js → index-7c6k4yvg.js} +2 -2
- package/{index-jk9q6pse.js → index-8hsbnyes.js} +1 -1
- package/{index-4yp06pn1.js → index-a00jg8x8.js} +2 -2
- package/{index-hjzr8ja1.js → index-bbkek1bp.js} +1 -1
- package/{index-r4w8cry5.js → index-bekddz9q.js} +4 -4
- package/{index-ev6rjw9n.js → index-e16gnn26.js} +35 -12
- package/{index-fmgbwsr5.js → index-ec09pws3.js} +4 -4
- package/{index-n0z3f2xg.js → index-ej426mcr.js} +4 -4
- package/{index-jtm86byd.js → index-hgna5rjy.js} +1 -1
- package/{index-e3qskyt2.js → index-k95xkh4p.js} +2 -2
- package/{index-b978vs2e.js → index-mr5s7mqd.js} +3 -3
- package/{index-6pz1j0zj.js → index-mxvakhsm.js} +6 -2
- package/{index-f4gqk0dd.js → index-nm6wghb6.js} +6 -6
- package/{index-4zgs8g1w.js → index-rzwcpcc3.js} +1 -1
- package/{index-sggm9qkv.js → index-t3723f99.js} +1 -1
- package/{index-xwgm94zr.js → index-tdbas9r0.js} +1 -1
- package/{index-vhv3xnjz.js → index-yzmhasdb.js} +6 -6
- package/{index-ktprgc5g.js → index-zasydjxd.js} +4 -4
- package/index.js +19 -19
- package/{library.command-pwfx8dx2.js → library.command-ebdj1vnm.js} +4 -4
- package/{localRegistry.command-bxhqmd1d.js → localRegistry.command-gb178ne8.js} +8 -8
- package/{module.command-0t3ewata.js → module.command-9c1bnsx4.js} +8 -8
- package/{package.command-7hmkxvpa.js → package.command-t93p4jkj.js} +4 -4
- package/package.json +2 -2
- package/{page.command-st6r4s0q.js → page.command-k3k0meh2.js} +4 -4
- package/{primitive.command-1qkxpzsn.js → primitive.command-de6nktdt.js} +9 -9
- package/{quality.command-hy5vt1bh.js → quality.command-z5kth2wa.js} +451 -48
- package/{repair.command-nakkq2zp.js → repair.command-ehf2g112.js} +6 -6
- package/{scalar.command-fjq272gs.js → scalar.command-7a2jtv33.js} +7 -7
- 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 +128 -7
- package/{workflow.command-jswbgk90.js → workflow.command-e0tt0vk2.js} +12 -12
- package/{workspace.command-p9r9nckg.js → workspace.command-fpda0evj.js} +21 -21
|
@@ -7,9 +7,9 @@ import {
|
|
|
7
7
|
command,
|
|
8
8
|
runner,
|
|
9
9
|
script
|
|
10
|
-
} from "./index-
|
|
11
|
-
import"./index-
|
|
12
|
-
import"./index-
|
|
10
|
+
} from "./index-8hsbnyes.js";
|
|
11
|
+
import"./index-hgna5rjy.js";
|
|
12
|
+
import"./index-mxvakhsm.js";
|
|
13
13
|
import"./index-67546d0j.js";
|
|
14
14
|
import"./index-r24hmh0q.js";
|
|
15
15
|
|
|
@@ -18,7 +18,369 @@ import { createHash } from "crypto";
|
|
|
18
18
|
import { readdir, readFile, stat } from "fs/promises";
|
|
19
19
|
import path from "path";
|
|
20
20
|
import ignore from "ignore";
|
|
21
|
+
import ts2 from "typescript";
|
|
22
|
+
|
|
23
|
+
// pkgs/@akanjs/devkit/ssrScanner.ts
|
|
21
24
|
import ts from "typescript";
|
|
25
|
+
var STATIC_COMPONENT_MIN_MASS = 4;
|
|
26
|
+
var MIXED_COMPONENT_MIN_MASS = 10;
|
|
27
|
+
var MIXED_COMPONENT_MAX_TOUCHES = 2;
|
|
28
|
+
var MODULE_SERVER_VIEW_MIN_CLIENT_MASS = 12;
|
|
29
|
+
|
|
30
|
+
class SsrScanner {
|
|
31
|
+
static #serverSafeCalls = new Set(["usePage", "getSelf", "useServer"]);
|
|
32
|
+
static #clientGlobals = new Set([
|
|
33
|
+
"window",
|
|
34
|
+
"document",
|
|
35
|
+
"navigator",
|
|
36
|
+
"localStorage",
|
|
37
|
+
"sessionStorage",
|
|
38
|
+
"location",
|
|
39
|
+
"history",
|
|
40
|
+
"screen",
|
|
41
|
+
"matchMedia",
|
|
42
|
+
"IntersectionObserver",
|
|
43
|
+
"ResizeObserver",
|
|
44
|
+
"MutationObserver",
|
|
45
|
+
"requestAnimationFrame",
|
|
46
|
+
"WebSocket"
|
|
47
|
+
]);
|
|
48
|
+
static #clientRuntimeImports = new Set(["st", "fetch"]);
|
|
49
|
+
scan(sourceFiles) {
|
|
50
|
+
const componentFiles = sourceFiles.filter((sourceFile) => this.#isBalancedFile(sourceFile.file));
|
|
51
|
+
return {
|
|
52
|
+
warnings: [
|
|
53
|
+
...componentFiles.flatMap((sourceFile) => this.#scanFile(sourceFile)),
|
|
54
|
+
...this.#scanModules(componentFiles)
|
|
55
|
+
],
|
|
56
|
+
balance: this.#measureBalance(componentFiles)
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
#scanFile(sourceFile) {
|
|
60
|
+
if (!this.#hasUseClient(sourceFile.sourceFile))
|
|
61
|
+
return [];
|
|
62
|
+
const vendorNames = this.#getVendorNames(sourceFile.sourceFile);
|
|
63
|
+
const hasVendorImport = this.#hasVendorImport(sourceFile.sourceFile);
|
|
64
|
+
const importsClientRuntime = this.#importsClientRuntime(sourceFile.sourceFile);
|
|
65
|
+
const components = this.#getComponents(sourceFile, vendorNames);
|
|
66
|
+
const warnings = [];
|
|
67
|
+
if (!hasVendorImport && !importsClientRuntime && this.#getTouches(sourceFile.sourceFile, sourceFile.sourceFile).length === 0 && !this.#isConventionClientFile(sourceFile.file)) {
|
|
68
|
+
warnings.push({
|
|
69
|
+
rule: "akan.ssr.unnecessary-use-client",
|
|
70
|
+
scope: "ssr",
|
|
71
|
+
severity: "warning",
|
|
72
|
+
file: sourceFile.file,
|
|
73
|
+
line: 1,
|
|
74
|
+
message: `"use client" is declared but the file uses no client-only capability (hook, event handler, store, or browser API).`
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
for (const component of components) {
|
|
78
|
+
if (component.vendorTags)
|
|
79
|
+
continue;
|
|
80
|
+
if (component.touches.length === 0 && component.mass >= STATIC_COMPONENT_MIN_MASS) {
|
|
81
|
+
warnings.push({
|
|
82
|
+
rule: "akan.ssr.client-static-component",
|
|
83
|
+
scope: "ssr",
|
|
84
|
+
severity: "warning",
|
|
85
|
+
file: sourceFile.file,
|
|
86
|
+
line: component.line,
|
|
87
|
+
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.`
|
|
88
|
+
});
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (component.touches.length >= 1 && component.touches.length <= MIXED_COMPONENT_MAX_TOUCHES && component.mass >= MIXED_COMPONENT_MIN_MASS) {
|
|
92
|
+
warnings.push({
|
|
93
|
+
rule: "akan.ssr.client-static-markup",
|
|
94
|
+
scope: "ssr",
|
|
95
|
+
severity: "warning",
|
|
96
|
+
file: sourceFile.file,
|
|
97
|
+
line: component.line,
|
|
98
|
+
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.`
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
warnings.push(...this.#getMountLoadWarnings(sourceFile));
|
|
103
|
+
warnings.push(...this.#getTemplateStateWarnings(sourceFile));
|
|
104
|
+
return warnings;
|
|
105
|
+
}
|
|
106
|
+
#scanModules(sourceFiles) {
|
|
107
|
+
const modules = new Map;
|
|
108
|
+
for (const sourceFile of sourceFiles) {
|
|
109
|
+
const moduleDir = this.#getModuleDir(sourceFile.file);
|
|
110
|
+
if (!moduleDir)
|
|
111
|
+
continue;
|
|
112
|
+
const entry = modules.get(moduleDir) ?? { clientMass: 0, serverFiles: 0, line: sourceFile.file };
|
|
113
|
+
if (this.#hasUseClient(sourceFile.sourceFile))
|
|
114
|
+
entry.clientMass += this.#getMass(sourceFile.sourceFile);
|
|
115
|
+
else if (/\.(Unit|View)\.tsx$/.test(sourceFile.file))
|
|
116
|
+
entry.serverFiles += 1;
|
|
117
|
+
modules.set(moduleDir, entry);
|
|
118
|
+
}
|
|
119
|
+
return [...modules].filter(([, entry]) => entry.serverFiles === 0 && entry.clientMass >= MODULE_SERVER_VIEW_MIN_CLIENT_MASS).map(([moduleDir, entry]) => ({
|
|
120
|
+
rule: "akan.ssr.module-missing-server-view",
|
|
121
|
+
scope: "ssr",
|
|
122
|
+
severity: "warning",
|
|
123
|
+
file: entry.line,
|
|
124
|
+
message: `Module "${moduleDir}" renders ${entry.clientMass} JSX elements from client files only; it declares no Unit or View server component.`
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
#getMountLoadWarnings(sourceFile) {
|
|
128
|
+
const warnings = [];
|
|
129
|
+
const visit = (node) => {
|
|
130
|
+
if (this.#isMountEffect(sourceFile.sourceFile, node)) {
|
|
131
|
+
for (const load of this.#getLoadCalls(sourceFile.sourceFile, node)) {
|
|
132
|
+
warnings.push({
|
|
133
|
+
rule: "akan.ssr.client-mount-load",
|
|
134
|
+
scope: "ssr",
|
|
135
|
+
severity: "warning",
|
|
136
|
+
file: sourceFile.file,
|
|
137
|
+
line: this.#getLine(sourceFile.sourceFile, load.node),
|
|
138
|
+
message: `Mount-only effect loads server data with ${load.callee}(). The route can fetch this before the first byte instead.`
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
ts.forEachChild(node, visit);
|
|
143
|
+
};
|
|
144
|
+
ts.forEachChild(sourceFile.sourceFile, visit);
|
|
145
|
+
return warnings;
|
|
146
|
+
}
|
|
147
|
+
#isMountEffect(sourceFile, node) {
|
|
148
|
+
if (!ts.isCallExpression(node))
|
|
149
|
+
return false;
|
|
150
|
+
const callee = node.expression.getText(sourceFile);
|
|
151
|
+
if (callee !== "useEffect" && callee !== "useLayoutEffect")
|
|
152
|
+
return false;
|
|
153
|
+
const deps = node.arguments[1];
|
|
154
|
+
return !!deps && ts.isArrayLiteralExpression(deps) && deps.elements.length === 0;
|
|
155
|
+
}
|
|
156
|
+
#getLoadCalls(sourceFile, node) {
|
|
157
|
+
const calls = [];
|
|
158
|
+
const visit = (child) => {
|
|
159
|
+
if (ts.isCallExpression(child)) {
|
|
160
|
+
const callee = child.expression.getText(sourceFile);
|
|
161
|
+
if (/^fetch\.[a-z]/.test(callee) || /^st\.do\.(init|get|view|load|list|count|insight)[A-Z]/.test(callee))
|
|
162
|
+
calls.push({ callee, node: child });
|
|
163
|
+
}
|
|
164
|
+
ts.forEachChild(child, visit);
|
|
165
|
+
};
|
|
166
|
+
ts.forEachChild(node, visit);
|
|
167
|
+
return calls;
|
|
168
|
+
}
|
|
169
|
+
#getTemplateStateWarnings(sourceFile) {
|
|
170
|
+
if (!sourceFile.file.endsWith(".Template.tsx"))
|
|
171
|
+
return [];
|
|
172
|
+
const warnings = [];
|
|
173
|
+
const visit = (node) => {
|
|
174
|
+
if (ts.isCallExpression(node) && node.expression.getText(sourceFile.sourceFile) === "useState") {
|
|
175
|
+
warnings.push({
|
|
176
|
+
rule: "akan.ssr.template-client-state",
|
|
177
|
+
scope: "ssr",
|
|
178
|
+
severity: "warning",
|
|
179
|
+
file: sourceFile.file,
|
|
180
|
+
line: this.#getLine(sourceFile.sourceFile, node),
|
|
181
|
+
message: "Template holds form state in useState. Templates are store-driven and carry no local state."
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
ts.forEachChild(node, visit);
|
|
185
|
+
};
|
|
186
|
+
ts.forEachChild(sourceFile.sourceFile, visit);
|
|
187
|
+
return warnings;
|
|
188
|
+
}
|
|
189
|
+
#measureBalance(sourceFiles) {
|
|
190
|
+
const scopes = new Map;
|
|
191
|
+
for (const sourceFile of sourceFiles) {
|
|
192
|
+
const segments = sourceFile.file.split("/");
|
|
193
|
+
const scope = `${segments[0]}/${segments[1]}`;
|
|
194
|
+
const entry = scopes.get(scope) ?? { serverMass: 0, clientMass: 0 };
|
|
195
|
+
const mass = this.#getMass(sourceFile.sourceFile);
|
|
196
|
+
if (this.#hasUseClient(sourceFile.sourceFile))
|
|
197
|
+
entry.clientMass += mass;
|
|
198
|
+
else
|
|
199
|
+
entry.serverMass += mass;
|
|
200
|
+
scopes.set(scope, entry);
|
|
201
|
+
}
|
|
202
|
+
const entries = [...scopes].map(([scope, mass]) => ({ scope, ...mass, serverShare: getShare(mass.serverMass, mass.clientMass) })).sort((a, b) => a.scope.localeCompare(b.scope));
|
|
203
|
+
if (entries.length < 2)
|
|
204
|
+
return entries;
|
|
205
|
+
const serverMass = entries.reduce((sum, entry) => sum + entry.serverMass, 0);
|
|
206
|
+
const clientMass = entries.reduce((sum, entry) => sum + entry.clientMass, 0);
|
|
207
|
+
return [...entries, { scope: "workspace", serverMass, clientMass, serverShare: getShare(serverMass, clientMass) }];
|
|
208
|
+
}
|
|
209
|
+
#getComponents(sourceFile, vendorNames) {
|
|
210
|
+
const components = [];
|
|
211
|
+
for (const statement of sourceFile.sourceFile.statements) {
|
|
212
|
+
for (const { name, node } of this.#getComponentNodes(statement)) {
|
|
213
|
+
if (!/^[A-Z]/.test(name))
|
|
214
|
+
continue;
|
|
215
|
+
components.push({
|
|
216
|
+
name,
|
|
217
|
+
line: this.#getLine(sourceFile.sourceFile, node),
|
|
218
|
+
mass: this.#getMass(node),
|
|
219
|
+
touches: this.#getTouches(sourceFile.sourceFile, node),
|
|
220
|
+
vendorTags: [...this.#getTagNames(sourceFile.sourceFile, node)].some((tag) => vendorNames.has(tag))
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return components;
|
|
225
|
+
}
|
|
226
|
+
#getComponentNodes(statement) {
|
|
227
|
+
if (ts.isFunctionDeclaration(statement) && statement.body)
|
|
228
|
+
return [{ name: statement.name?.text ?? "default", node: statement.body }];
|
|
229
|
+
if (!ts.isVariableStatement(statement))
|
|
230
|
+
return [];
|
|
231
|
+
return statement.declarationList.declarations.flatMap((declaration) => {
|
|
232
|
+
if (!ts.isIdentifier(declaration.name) || !declaration.initializer)
|
|
233
|
+
return [];
|
|
234
|
+
if (!ts.isArrowFunction(declaration.initializer) && !ts.isFunctionExpression(declaration.initializer))
|
|
235
|
+
return [];
|
|
236
|
+
return [{ name: declaration.name.text, node: declaration.initializer }];
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
#getTouches(sourceFile, node) {
|
|
240
|
+
const touches = [];
|
|
241
|
+
const visit = (child) => {
|
|
242
|
+
if (ts.isCallExpression(child)) {
|
|
243
|
+
const callee = child.expression.getText(sourceFile);
|
|
244
|
+
const bareName = callee.split(".").pop() ?? callee;
|
|
245
|
+
if (callee === "createContext" || callee === "lazy")
|
|
246
|
+
touches.push(callee);
|
|
247
|
+
else if (/^use[A-Z]/.test(bareName) && !SsrScanner.#serverSafeCalls.has(bareName))
|
|
248
|
+
touches.push(bareName);
|
|
249
|
+
}
|
|
250
|
+
if (ts.isJsxAttribute(child) && /^on[A-Z]/.test(child.name.getText(sourceFile)))
|
|
251
|
+
touches.push(child.name.getText(sourceFile));
|
|
252
|
+
if (ts.isPropertyAccessExpression(child)) {
|
|
253
|
+
const root = getAccessRoot(child);
|
|
254
|
+
if (root === "st")
|
|
255
|
+
touches.push("st");
|
|
256
|
+
else if (SsrScanner.#clientGlobals.has(root))
|
|
257
|
+
touches.push(root);
|
|
258
|
+
}
|
|
259
|
+
ts.forEachChild(child, visit);
|
|
260
|
+
};
|
|
261
|
+
ts.forEachChild(node, visit);
|
|
262
|
+
return touches;
|
|
263
|
+
}
|
|
264
|
+
#getTagNames(sourceFile, node) {
|
|
265
|
+
const tags = new Set;
|
|
266
|
+
const visit = (child) => {
|
|
267
|
+
if (ts.isJsxOpeningElement(child) || ts.isJsxSelfClosingElement(child))
|
|
268
|
+
tags.add(child.tagName.getText(sourceFile).split(".")[0]);
|
|
269
|
+
ts.forEachChild(child, visit);
|
|
270
|
+
};
|
|
271
|
+
ts.forEachChild(node, visit);
|
|
272
|
+
return tags;
|
|
273
|
+
}
|
|
274
|
+
#getMass(node) {
|
|
275
|
+
let mass = 0;
|
|
276
|
+
const visit = (child) => {
|
|
277
|
+
if (ts.isJsxOpeningElement(child) || ts.isJsxSelfClosingElement(child))
|
|
278
|
+
mass += 1;
|
|
279
|
+
ts.forEachChild(child, visit);
|
|
280
|
+
};
|
|
281
|
+
ts.forEachChild(node, visit);
|
|
282
|
+
return mass;
|
|
283
|
+
}
|
|
284
|
+
#hasUseClient(sourceFile) {
|
|
285
|
+
const first = sourceFile.statements[0];
|
|
286
|
+
if (!first || !ts.isExpressionStatement(first) || !ts.isStringLiteral(first.expression))
|
|
287
|
+
return false;
|
|
288
|
+
return first.expression.text === "use client";
|
|
289
|
+
}
|
|
290
|
+
#hasVendorImport(sourceFile) {
|
|
291
|
+
return sourceFile.statements.some((statement) => ts.isImportDeclaration(statement) && isVendorSpecifier(getSpecifier(statement)));
|
|
292
|
+
}
|
|
293
|
+
#getVendorNames(sourceFile) {
|
|
294
|
+
const names = new Set;
|
|
295
|
+
for (const statement of sourceFile.statements) {
|
|
296
|
+
if (!ts.isImportDeclaration(statement) || !isVendorSpecifier(getSpecifier(statement)))
|
|
297
|
+
continue;
|
|
298
|
+
const clause = statement.importClause;
|
|
299
|
+
if (clause?.name)
|
|
300
|
+
names.add(clause.name.text);
|
|
301
|
+
if (clause?.namedBindings && ts.isNamespaceImport(clause.namedBindings))
|
|
302
|
+
names.add(clause.namedBindings.name.text);
|
|
303
|
+
if (clause?.namedBindings && ts.isNamedImports(clause.namedBindings))
|
|
304
|
+
for (const element of clause.namedBindings.elements)
|
|
305
|
+
names.add(element.name.text);
|
|
306
|
+
}
|
|
307
|
+
return names;
|
|
308
|
+
}
|
|
309
|
+
#importsClientRuntime(sourceFile) {
|
|
310
|
+
for (const statement of sourceFile.statements) {
|
|
311
|
+
if (!ts.isImportDeclaration(statement))
|
|
312
|
+
continue;
|
|
313
|
+
const bindings = statement.importClause?.namedBindings;
|
|
314
|
+
if (!bindings || !ts.isNamedImports(bindings))
|
|
315
|
+
continue;
|
|
316
|
+
if (bindings.elements.some((element) => SsrScanner.#clientRuntimeImports.has(element.name.text)))
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
#isConventionClientFile(file) {
|
|
322
|
+
if (file.endsWith("/index_.tsx"))
|
|
323
|
+
return true;
|
|
324
|
+
return /\.(Zone|Template|Util)\.tsx$/.test(file) && this.#getModuleDir(file) !== null;
|
|
325
|
+
}
|
|
326
|
+
#isBalancedFile(file) {
|
|
327
|
+
if (!file.endsWith(".tsx") || file.endsWith(".test.tsx") || file.endsWith(".spec.tsx"))
|
|
328
|
+
return false;
|
|
329
|
+
const segments = file.split("/");
|
|
330
|
+
if (segments[0] !== "apps" && segments[0] !== "libs")
|
|
331
|
+
return false;
|
|
332
|
+
return segments[2] === "ui" || segments[2] === "lib";
|
|
333
|
+
}
|
|
334
|
+
#getModuleDir(file) {
|
|
335
|
+
const segments = file.split("/");
|
|
336
|
+
const libIndex = segments.indexOf("lib");
|
|
337
|
+
if (libIndex < 1 || segments.length <= libIndex + 2)
|
|
338
|
+
return null;
|
|
339
|
+
const moduleName = segments[libIndex + 1];
|
|
340
|
+
if (moduleName.startsWith("_"))
|
|
341
|
+
return null;
|
|
342
|
+
return segments.slice(0, libIndex + 2).join("/");
|
|
343
|
+
}
|
|
344
|
+
#getLine(sourceFile, node) {
|
|
345
|
+
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
var SSR_SERVER_SHARE_TARGET = 0.5;
|
|
349
|
+
function formatSsrBalance(balance) {
|
|
350
|
+
if (balance.length === 0)
|
|
351
|
+
return ["No component files found."];
|
|
352
|
+
return balance.map((entry) => {
|
|
353
|
+
const total = entry.serverMass + entry.clientMass;
|
|
354
|
+
const share = `${Math.round(entry.serverShare * 100)}% server`;
|
|
355
|
+
const counts = `${entry.serverMass} of ${total} JSX elements, ${entry.clientMass} client`;
|
|
356
|
+
const flag = entry.serverShare < SSR_SERVER_SHARE_TARGET ? ` <- below the ${Math.round(SSR_SERVER_SHARE_TARGET * 100)}% target` : "";
|
|
357
|
+
return ` ${entry.scope}: ${share} (${counts})${flag}`;
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
function getShare(serverMass, clientMass) {
|
|
361
|
+
const total = serverMass + clientMass;
|
|
362
|
+
return total === 0 ? 1 : serverMass / total;
|
|
363
|
+
}
|
|
364
|
+
function getSpecifier(statement) {
|
|
365
|
+
return ts.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : "";
|
|
366
|
+
}
|
|
367
|
+
function isVendorSpecifier(specifier) {
|
|
368
|
+
if (specifier === "" || specifier.startsWith(".") || specifier.startsWith("/"))
|
|
369
|
+
return false;
|
|
370
|
+
if (specifier === "react" || specifier === "react-dom" || specifier.startsWith("react/"))
|
|
371
|
+
return false;
|
|
372
|
+
if (specifier.startsWith("node:"))
|
|
373
|
+
return false;
|
|
374
|
+
return !/^(akanjs|@akanjs|@libs|@apps|@contract)(\/|$)/.test(specifier);
|
|
375
|
+
}
|
|
376
|
+
function getAccessRoot(node) {
|
|
377
|
+
let current = node;
|
|
378
|
+
while (ts.isPropertyAccessExpression(current))
|
|
379
|
+
current = current.expression;
|
|
380
|
+
return ts.isIdentifier(current) ? current.text : "";
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// pkgs/@akanjs/devkit/qualityScanner.ts
|
|
22
384
|
var MAX_FILE_LINES = 2000;
|
|
23
385
|
var PLACEHOLDER_EXPORT_NAMES = new Set([
|
|
24
386
|
"aa",
|
|
@@ -101,7 +463,13 @@ var RULE_FIXES = {
|
|
|
101
463
|
"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
464
|
"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
465
|
"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."
|
|
466
|
+
"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.",
|
|
467
|
+
"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.',
|
|
468
|
+
"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.",
|
|
469
|
+
"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.",
|
|
470
|
+
"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.",
|
|
471
|
+
"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.",
|
|
472
|
+
"akan.ssr.template-client-state": "Bind the field to the store instead: `value={xForm.field}` with `onChange={st.do.setFieldOnX}`."
|
|
105
473
|
};
|
|
106
474
|
function getRuleFix(rule) {
|
|
107
475
|
if (rule.startsWith("akan.convention"))
|
|
@@ -114,18 +482,21 @@ class AkanQualityScanner {
|
|
|
114
482
|
const targetFiles = await this.#collectTargetFiles(workspaceRoot);
|
|
115
483
|
const sourceFiles = await Promise.all(targetFiles.filter((file) => !AbstractDoc.isAbstractPath(file)).map((file) => this.#readSourceFile(workspaceRoot, file)));
|
|
116
484
|
const abstractFiles = await Promise.all(targetFiles.filter((file) => AbstractDoc.isAbstractPath(file)).map((file) => this.#readTextFile(workspaceRoot, file)));
|
|
485
|
+
const ssr = new SsrScanner().scan(sourceFiles);
|
|
117
486
|
const warnings = [
|
|
118
487
|
...this.#scanGlobalQuality(sourceFiles),
|
|
119
488
|
...sourceFiles.flatMap((sourceFile) => this.#scanSingleFileQuality(sourceFile)),
|
|
120
489
|
...sourceFiles.flatMap((sourceFile) => this.#scanComponentQuality(sourceFile)),
|
|
121
490
|
...sourceFiles.flatMap((sourceFile) => this.#scanConventionQuality(sourceFile)),
|
|
122
491
|
...sourceFiles.flatMap((sourceFile) => this.#scanLayoutQuality(sourceFile)),
|
|
123
|
-
...abstractFiles.flatMap((abstractFile) => this.#scanAbstractQuality(abstractFile))
|
|
492
|
+
...abstractFiles.flatMap((abstractFile) => this.#scanAbstractQuality(abstractFile)),
|
|
493
|
+
...ssr.warnings
|
|
124
494
|
];
|
|
125
495
|
return {
|
|
126
496
|
workspaceRoot,
|
|
127
497
|
scannedFiles: sourceFiles.length + abstractFiles.length,
|
|
128
498
|
warnings: warnings.map((warning) => ({ ...warning, fix: warning.fix ?? getRuleFix(warning.rule) })).sort(compareWarnings),
|
|
499
|
+
ssrBalance: ssr.balance,
|
|
129
500
|
suggestedRules: SUGGESTED_RULES
|
|
130
501
|
};
|
|
131
502
|
}
|
|
@@ -171,7 +542,7 @@ class AkanQualityScanner {
|
|
|
171
542
|
file,
|
|
172
543
|
absolutePath,
|
|
173
544
|
content,
|
|
174
|
-
sourceFile:
|
|
545
|
+
sourceFile: ts2.createSourceFile(file, content, ts2.ScriptTarget.Latest, true, getScriptKind(file))
|
|
175
546
|
};
|
|
176
547
|
}
|
|
177
548
|
async#readTextFile(workspaceRoot, file) {
|
|
@@ -368,6 +739,10 @@ function formatQualityScanResult(result) {
|
|
|
368
739
|
"",
|
|
369
740
|
...formatQualityWarnings(result.warnings),
|
|
370
741
|
"",
|
|
742
|
+
"SSR balance (component files, JSX elements rendered per side):",
|
|
743
|
+
"",
|
|
744
|
+
...formatSsrBalance(result.ssrBalance),
|
|
745
|
+
"",
|
|
371
746
|
"Suggested quality rules:",
|
|
372
747
|
"",
|
|
373
748
|
...result.suggestedRules.map((rule) => ` - ${rule}`)
|
|
@@ -375,6 +750,24 @@ function formatQualityScanResult(result) {
|
|
|
375
750
|
return sections.join(`
|
|
376
751
|
`);
|
|
377
752
|
}
|
|
753
|
+
function formatSsrScanResult(result) {
|
|
754
|
+
const sections = [
|
|
755
|
+
"Akan SSR Balance Scan",
|
|
756
|
+
`workspace: ${result.workspaceRoot}`,
|
|
757
|
+
`scanned files: ${result.scannedFiles}`,
|
|
758
|
+
`ssr warnings: ${result.warnings.length}`,
|
|
759
|
+
"",
|
|
760
|
+
"Server render share (component files, JSX elements rendered per side):",
|
|
761
|
+
"",
|
|
762
|
+
...formatSsrBalance(result.ssrBalance),
|
|
763
|
+
"",
|
|
764
|
+
"Warnings:",
|
|
765
|
+
"",
|
|
766
|
+
...formatQualityWarnings(result.warnings)
|
|
767
|
+
];
|
|
768
|
+
return sections.join(`
|
|
769
|
+
`);
|
|
770
|
+
}
|
|
378
771
|
function formatQualityWarnings(warnings) {
|
|
379
772
|
if (warnings.length === 0)
|
|
380
773
|
return ["No warnings found."];
|
|
@@ -396,7 +789,7 @@ function getExportedFunctionLikes(sourceFile) {
|
|
|
396
789
|
const declarations = [];
|
|
397
790
|
const nameExempt = isPageRouteFile(sourceFile.file) || isUiComponentFile(sourceFile.file);
|
|
398
791
|
for (const statement of sourceFile.sourceFile.statements) {
|
|
399
|
-
if (
|
|
792
|
+
if (ts2.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
|
|
400
793
|
declarations.push({
|
|
401
794
|
name: statement.name.text,
|
|
402
795
|
kind: "function",
|
|
@@ -406,7 +799,7 @@ function getExportedFunctionLikes(sourceFile) {
|
|
|
406
799
|
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false)
|
|
407
800
|
});
|
|
408
801
|
}
|
|
409
|
-
if (
|
|
802
|
+
if (ts2.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
410
803
|
declarations.push({
|
|
411
804
|
name: statement.name.text,
|
|
412
805
|
kind: "class",
|
|
@@ -416,9 +809,9 @@ function getExportedFunctionLikes(sourceFile) {
|
|
|
416
809
|
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, isEnumClassStatement(sourceFile.sourceFile, statement))
|
|
417
810
|
});
|
|
418
811
|
}
|
|
419
|
-
if (
|
|
812
|
+
if (ts2.isVariableStatement(statement) && isExported(statement)) {
|
|
420
813
|
for (const declaration of statement.declarationList.declarations) {
|
|
421
|
-
if (!
|
|
814
|
+
if (!ts2.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
|
|
422
815
|
continue;
|
|
423
816
|
declarations.push({
|
|
424
817
|
name: declaration.name.text,
|
|
@@ -457,14 +850,14 @@ function isInLibModule(file) {
|
|
|
457
850
|
return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
|
|
458
851
|
}
|
|
459
852
|
function isEnumClassStatement(sourceFile, statement) {
|
|
460
|
-
if (!
|
|
853
|
+
if (!ts2.isClassDeclaration(statement))
|
|
461
854
|
return false;
|
|
462
|
-
const heritageClause = statement.heritageClauses?.find((clause) => clause.token ===
|
|
855
|
+
const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts2.SyntaxKind.ExtendsKeyword);
|
|
463
856
|
const expression = heritageClause?.types[0]?.expression;
|
|
464
857
|
return !!expression && expression.getText(sourceFile).startsWith("enumOf(");
|
|
465
858
|
}
|
|
466
859
|
function getExportedClassNames(sourceFile) {
|
|
467
|
-
return sourceFile.statements.filter((statement) =>
|
|
860
|
+
return sourceFile.statements.filter((statement) => ts2.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
|
|
468
861
|
}
|
|
469
862
|
function isComponentDeclarationFile(file) {
|
|
470
863
|
if (!file.endsWith(".tsx"))
|
|
@@ -480,7 +873,7 @@ function isComponentDeclarationFile(file) {
|
|
|
480
873
|
function getComponentFileDeclarations(sourceFile) {
|
|
481
874
|
const reExportedNames = new Set;
|
|
482
875
|
for (const statement of sourceFile.statements) {
|
|
483
|
-
if (
|
|
876
|
+
if (ts2.isExportDeclaration(statement) && statement.exportClause && ts2.isNamedExports(statement.exportClause)) {
|
|
484
877
|
for (const element of statement.exportClause.elements)
|
|
485
878
|
reExportedNames.add((element.propertyName ?? element.name).text);
|
|
486
879
|
}
|
|
@@ -490,19 +883,19 @@ function getComponentFileDeclarations(sourceFile) {
|
|
|
490
883
|
const isDefaultExport = isDefaultExportStatement(statement);
|
|
491
884
|
const inlineExported = isExported(statement);
|
|
492
885
|
const add = (name, kind, line) => declarations.push({ name, kind, line, exported: inlineExported || reExportedNames.has(name), isDefaultExport });
|
|
493
|
-
if (
|
|
886
|
+
if (ts2.isInterfaceDeclaration(statement))
|
|
494
887
|
add(statement.name.text, "interface", getLine(sourceFile, statement));
|
|
495
|
-
else if (
|
|
888
|
+
else if (ts2.isTypeAliasDeclaration(statement))
|
|
496
889
|
add(statement.name.text, "type", getLine(sourceFile, statement));
|
|
497
|
-
else if (
|
|
890
|
+
else if (ts2.isEnumDeclaration(statement))
|
|
498
891
|
add(statement.name.text, "enum", getLine(sourceFile, statement));
|
|
499
|
-
else if (
|
|
892
|
+
else if (ts2.isFunctionDeclaration(statement) && statement.name)
|
|
500
893
|
add(statement.name.text, "function", getLine(sourceFile, statement));
|
|
501
|
-
else if (
|
|
894
|
+
else if (ts2.isClassDeclaration(statement) && statement.name)
|
|
502
895
|
add(statement.name.text, "class", getLine(sourceFile, statement));
|
|
503
|
-
else if (
|
|
896
|
+
else if (ts2.isVariableStatement(statement)) {
|
|
504
897
|
for (const declaration of statement.declarationList.declarations) {
|
|
505
|
-
if (!
|
|
898
|
+
if (!ts2.isIdentifier(declaration.name))
|
|
506
899
|
continue;
|
|
507
900
|
const kind = isFunctionLikeInitializer(declaration.initializer) ? "function" : "variable";
|
|
508
901
|
add(declaration.name.text, kind, getLine(sourceFile, declaration));
|
|
@@ -514,15 +907,15 @@ function getComponentFileDeclarations(sourceFile) {
|
|
|
514
907
|
function getCompoundComponentNames(sourceFile) {
|
|
515
908
|
const names = new Set;
|
|
516
909
|
for (const statement of sourceFile.statements) {
|
|
517
|
-
if (!
|
|
910
|
+
if (!ts2.isExpressionStatement(statement))
|
|
518
911
|
continue;
|
|
519
912
|
const { expression } = statement;
|
|
520
|
-
if (!
|
|
913
|
+
if (!ts2.isBinaryExpression(expression) || expression.operatorToken.kind !== ts2.SyntaxKind.EqualsToken)
|
|
521
914
|
continue;
|
|
522
|
-
if (!
|
|
915
|
+
if (!ts2.isPropertyAccessExpression(expression.left) || !isPascalCaseName(expression.left.name.text))
|
|
523
916
|
continue;
|
|
524
917
|
names.add(expression.left.name.text);
|
|
525
|
-
if (
|
|
918
|
+
if (ts2.isIdentifier(expression.right) && isPascalCaseName(expression.right.text))
|
|
526
919
|
names.add(expression.right.text);
|
|
527
920
|
}
|
|
528
921
|
return names;
|
|
@@ -542,9 +935,9 @@ function isPascalCaseName(name) {
|
|
|
542
935
|
return /^[A-Z]/.test(name) && !/^[A-Z0-9_]+$/.test(name);
|
|
543
936
|
}
|
|
544
937
|
function isDefaultExportStatement(statement) {
|
|
545
|
-
if (
|
|
938
|
+
if (ts2.isExportAssignment(statement))
|
|
546
939
|
return !statement.isExportEquals;
|
|
547
|
-
return !!(
|
|
940
|
+
return !!(ts2.getCombinedModifierFlags(statement) & ts2.ModifierFlags.Default);
|
|
548
941
|
}
|
|
549
942
|
function getPlaceholderExportWarnings(sourceFile) {
|
|
550
943
|
if (!sourceFile.file.endsWith("/index.ts") && !sourceFile.file.endsWith("/index.tsx"))
|
|
@@ -574,7 +967,7 @@ function getDictionaryTextWarnings(sourceFile) {
|
|
|
574
967
|
function getGlobalMutationWarnings(sourceFile) {
|
|
575
968
|
const warnings = [];
|
|
576
969
|
for (const statement of sourceFile.sourceFile.statements) {
|
|
577
|
-
if (
|
|
970
|
+
if (ts2.isModuleDeclaration(statement) && statement.name.getText(sourceFile.sourceFile) === "global") {
|
|
578
971
|
warnings.push({
|
|
579
972
|
rule: "akan.file.global-declaration",
|
|
580
973
|
scope: "file",
|
|
@@ -584,7 +977,7 @@ function getGlobalMutationWarnings(sourceFile) {
|
|
|
584
977
|
message: "Global declarations require an explicit low-level integration allowlist."
|
|
585
978
|
});
|
|
586
979
|
}
|
|
587
|
-
if (
|
|
980
|
+
if (ts2.isInterfaceDeclaration(statement) && statement.name.text === "Window") {
|
|
588
981
|
warnings.push({
|
|
589
982
|
rule: "akan.file.window-augmentation",
|
|
590
983
|
scope: "file",
|
|
@@ -594,7 +987,7 @@ function getGlobalMutationWarnings(sourceFile) {
|
|
|
594
987
|
message: "Window augmentation should be isolated to approved browser integration files."
|
|
595
988
|
});
|
|
596
989
|
}
|
|
597
|
-
if (
|
|
990
|
+
if (ts2.isExpressionStatement(statement) && statement.expression.getText(sourceFile.sourceFile).includes(".prototype.")) {
|
|
598
991
|
warnings.push({
|
|
599
992
|
rule: "akan.file.prototype-mutation",
|
|
600
993
|
scope: "file",
|
|
@@ -612,23 +1005,23 @@ function getTopLevelDeclarations(sourceFile) {
|
|
|
612
1005
|
}
|
|
613
1006
|
function getTopLevelDeclaration(sourceFile, statement) {
|
|
614
1007
|
const line = getLine(sourceFile, statement);
|
|
615
|
-
if (
|
|
1008
|
+
if (ts2.isClassDeclaration(statement) && statement.name) {
|
|
616
1009
|
return [{ name: statement.name.text, kind: "class", line, exported: isExported(statement), node: statement }];
|
|
617
1010
|
}
|
|
618
|
-
if (
|
|
1011
|
+
if (ts2.isFunctionDeclaration(statement) && statement.name) {
|
|
619
1012
|
return [{ name: statement.name.text, kind: "function", line, exported: isExported(statement), node: statement }];
|
|
620
1013
|
}
|
|
621
|
-
if (
|
|
1014
|
+
if (ts2.isInterfaceDeclaration(statement)) {
|
|
622
1015
|
return [{ name: statement.name.text, kind: "interface", line, exported: isExported(statement), node: statement }];
|
|
623
1016
|
}
|
|
624
|
-
if (
|
|
1017
|
+
if (ts2.isTypeAliasDeclaration(statement)) {
|
|
625
1018
|
return [{ name: statement.name.text, kind: "type", line, exported: isExported(statement), node: statement }];
|
|
626
1019
|
}
|
|
627
|
-
if (
|
|
1020
|
+
if (ts2.isEnumDeclaration(statement)) {
|
|
628
1021
|
return [{ name: statement.name.text, kind: "enum", line, exported: isExported(statement), node: statement }];
|
|
629
1022
|
}
|
|
630
|
-
if (
|
|
631
|
-
return statement.declarationList.declarations.filter((declaration) =>
|
|
1023
|
+
if (ts2.isVariableStatement(statement)) {
|
|
1024
|
+
return statement.declarationList.declarations.filter((declaration) => ts2.isIdentifier(declaration.name)).map((declaration) => ({
|
|
632
1025
|
name: declaration.name.text,
|
|
633
1026
|
kind: "variable",
|
|
634
1027
|
line: getLine(sourceFile, declaration),
|
|
@@ -636,7 +1029,7 @@ function getTopLevelDeclaration(sourceFile, statement) {
|
|
|
636
1029
|
node: statement
|
|
637
1030
|
}));
|
|
638
1031
|
}
|
|
639
|
-
if (
|
|
1032
|
+
if (ts2.isExportDeclaration(statement)) {
|
|
640
1033
|
return [{ name: "export declaration", kind: "export", line, exported: true, node: statement }];
|
|
641
1034
|
}
|
|
642
1035
|
return [];
|
|
@@ -661,9 +1054,9 @@ function isAllowedConstantDeclaration(modelName, declaration) {
|
|
|
661
1054
|
return false;
|
|
662
1055
|
if ([`${modelName}Input`, `${modelName}Object`, modelName, `Light${modelName}`, `${modelName}Insight`].includes(declaration.name))
|
|
663
1056
|
return true;
|
|
664
|
-
if (!
|
|
1057
|
+
if (!ts2.isClassDeclaration(declaration.node))
|
|
665
1058
|
return false;
|
|
666
|
-
const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token ===
|
|
1059
|
+
const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token === ts2.SyntaxKind.ExtendsKeyword);
|
|
667
1060
|
const expression = heritageClause?.types[0]?.expression;
|
|
668
1061
|
return !!expression && expression.getText().startsWith("enumOf(");
|
|
669
1062
|
}
|
|
@@ -729,13 +1122,13 @@ function getModuleInfo(file) {
|
|
|
729
1122
|
return { moduleName, fileName, kind: "database" };
|
|
730
1123
|
}
|
|
731
1124
|
function isExportedConst(declaration) {
|
|
732
|
-
return declaration.exported &&
|
|
1125
|
+
return declaration.exported && ts2.isVariableStatement(declaration.node) && (declaration.node.declarationList.flags & ts2.NodeFlags.Const) !== 0;
|
|
733
1126
|
}
|
|
734
1127
|
function isExported(node) {
|
|
735
|
-
return !!
|
|
1128
|
+
return !!ts2.getCombinedModifierFlags(node) && !!(ts2.getCombinedModifierFlags(node) & ts2.ModifierFlags.Export);
|
|
736
1129
|
}
|
|
737
1130
|
function isFunctionLikeInitializer(node) {
|
|
738
|
-
return !!node && (
|
|
1131
|
+
return !!node && (ts2.isArrowFunction(node) || ts2.isFunctionExpression(node));
|
|
739
1132
|
}
|
|
740
1133
|
function getBodyFingerprint(sourceFile, node) {
|
|
741
1134
|
if (!node)
|
|
@@ -757,7 +1150,7 @@ async function isDirectory(absolutePath) {
|
|
|
757
1150
|
}
|
|
758
1151
|
}
|
|
759
1152
|
function getScriptKind(file) {
|
|
760
|
-
return file.endsWith(".tsx") ?
|
|
1153
|
+
return file.endsWith(".tsx") ? ts2.ScriptKind.TSX : ts2.ScriptKind.TS;
|
|
761
1154
|
}
|
|
762
1155
|
function getLine(sourceFile, node) {
|
|
763
1156
|
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
@@ -810,6 +1203,13 @@ class QualityScript extends script("quality", [QualityRunner]) {
|
|
|
810
1203
|
spinner.succeed(`Quality scan completed (${result.scannedFiles} files, ${result.warnings.length} warnings)`);
|
|
811
1204
|
Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatQualityScanResult(result));
|
|
812
1205
|
}
|
|
1206
|
+
async ssr(workspace, format = "text") {
|
|
1207
|
+
const spinner = workspace.spinning("Measuring Akan server/client render balance...");
|
|
1208
|
+
const scanned = await this.qualityRunner.scan(workspace);
|
|
1209
|
+
const result = { ...scanned, warnings: scanned.warnings.filter((warning) => warning.scope === "ssr") };
|
|
1210
|
+
spinner.succeed(`SSR scan completed (${result.scannedFiles} files, ${result.warnings.length} warnings)`);
|
|
1211
|
+
Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatSsrScanResult(result));
|
|
1212
|
+
}
|
|
813
1213
|
}
|
|
814
1214
|
|
|
815
1215
|
// pkgs/@akanjs/cli/quality/quality.command.ts
|
|
@@ -817,15 +1217,18 @@ class QualityCommand extends command("quality", [QualityScript], ({ public: targ
|
|
|
817
1217
|
quality: target({ desc: "Scan apps and libs for Akan code quality warnings" }).arg("action", String, {
|
|
818
1218
|
desc: "quality action",
|
|
819
1219
|
default: "scan",
|
|
820
|
-
enum: ["scan"]
|
|
1220
|
+
enum: ["scan", "ssr"]
|
|
821
1221
|
}).option("format", String, {
|
|
822
1222
|
desc: "output format",
|
|
823
1223
|
default: "text",
|
|
824
1224
|
enum: ["text", "json"]
|
|
825
1225
|
}).with(Workspace).exec(async function(action, format, workspace) {
|
|
826
|
-
if (action
|
|
827
|
-
|
|
828
|
-
|
|
1226
|
+
if (action === "scan")
|
|
1227
|
+
await this.qualityScript.scan(workspace, format);
|
|
1228
|
+
else if (action === "ssr")
|
|
1229
|
+
await this.qualityScript.ssr(workspace, format);
|
|
1230
|
+
else
|
|
1231
|
+
throw new Error(`Unknown quality action: ${action}. Use "scan" or "ssr".`);
|
|
829
1232
|
})
|
|
830
1233
|
})) {
|
|
831
1234
|
}
|