@akanjs/cli 3.0.0-alpha.2 → 3.0.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.build-stamp +1 -1
- package/{abstractCompactor-e5yq60qz.js → abstractCompactor-rk6twzvj.js} +2 -2
- package/{agent.command-yv00p0s0.js → agent.command-jfz8yfwy.js} +7 -7
- package/{application.command-0qsczf4k.js → application.command-mxyqtd6p.js} +5 -5
- package/{applicationBuildRunner-ct2pxyvw.js → applicationBuildRunner-cwk52sb7.js} +2 -2
- package/{applicationReleasePackager-gg8ccbeh.js → applicationReleasePackager-yrrhbkv3.js} +1 -1
- package/buildBatch.proc.js +3 -3
- package/{capacitorApp-4r9sm3qg.js → capacitorApp-j357ak1g.js} +5 -3
- package/{cloud.command-dxfv18sa.js → cloud.command-f87dwe52.js} +9 -9
- package/{context.command-8d9e3k6b.js → context.command-z5bf8bn7.js} +15 -15
- package/{guideline.command-3jbkxzh6.js → guideline.command-1m1ctar3.js} +5 -5
- package/incrementalBuilder.proc.js +2 -2
- package/{index-1pvex4az.js → index-1s71fwrq.js} +11 -11
- package/{index-0cj2zxbm.js → index-3pgtw35d.js} +1 -1
- package/{index-a00jg8x8.js → index-4yp06pn1.js} +2 -2
- package/{index-rzwcpcc3.js → index-4zgs8g1w.js} +1 -1
- package/{index-mxvakhsm.js → index-6pz1j0zj.js} +2 -6
- package/{index-mr5s7mqd.js → index-b978vs2e.js} +3 -3
- package/{index-pyt2b4nw.js → index-cap2k2ch.js} +2 -2
- package/{index-7c6k4yvg.js → index-cn24dcqn.js} +2 -2
- package/{index-2q1f5j9k.js → index-csqk9z5j.js} +3 -3
- package/{index-k95xkh4p.js → index-e3qskyt2.js} +2 -2
- package/{index-ec09pws3.js → index-fmgbwsr5.js} +4 -4
- package/{index-bbkek1bp.js → index-hjzr8ja1.js} +1 -1
- package/{index-8hsbnyes.js → index-jk9q6pse.js} +1 -1
- package/{index-hgna5rjy.js → index-jtm86byd.js} +1 -1
- package/{index-ej426mcr.js → index-n0z3f2xg.js} +4 -4
- package/{index-bekddz9q.js → index-r4w8cry5.js} +4 -4
- package/{index-e16gnn26.js → index-tq1y56tp.js} +12 -35
- package/{index-yzmhasdb.js → index-vhv3xnjz.js} +6 -6
- package/{index-zasydjxd.js → index-xj8bks3p.js} +4 -4
- package/{index-tdbas9r0.js → index-xwgm94zr.js} +1 -1
- package/{index-t3723f99.js → index-y0k42k04.js} +86 -17
- package/{index-nm6wghb6.js → index-ybybkec0.js} +6 -6
- package/index.js +19 -19
- package/{library.command-ebdj1vnm.js → library.command-pwfx8dx2.js} +4 -4
- package/{localRegistry.command-gb178ne8.js → localRegistry.command-r4zjx5py.js} +8 -8
- package/{module.command-9c1bnsx4.js → module.command-0t3ewata.js} +8 -8
- package/{package.command-t93p4jkj.js → package.command-7hmkxvpa.js} +4 -4
- package/package.json +2 -2
- package/{page.command-k3k0meh2.js → page.command-st6r4s0q.js} +4 -4
- package/{primitive.command-de6nktdt.js → primitive.command-1qkxpzsn.js} +9 -9
- package/{quality.command-z5kth2wa.js → quality.command-hy5vt1bh.js} +48 -451
- package/{repair.command-ehf2g112.js → repair.command-nakkq2zp.js} +6 -6
- package/{scalar.command-7a2jtv33.js → scalar.command-fjq272gs.js} +7 -7
- package/templates/lib/db.ts +1 -3
- package/templates/libRoot/base/baseLogic.ts +14 -0
- package/templates/libRoot/base/index.ts +8 -0
- package/templates/workspaceRoot/.cursor/rules/client-server-boundaries.mdc.template +0 -1
- package/templates/workspaceRoot/AGENTS.md.template +24 -148
- package/templates/workspaceRoot/biome.json.template +19 -0
- package/templates/workspaceRoot/docs/AI-DEVELOPMENT.md.template +4 -1
- package/{workflow.command-e0tt0vk2.js → workflow.command-jswbgk90.js} +12 -12
- package/{workspace.command-fpda0evj.js → workspace.command-h9wwcjpt.js} +21 -21
- package/guidelines/ssrRule/ssrRule.generate.json +0 -31
- package/guidelines/ssrRule/ssrRule.instruction.md +0 -108
- package/templates/workspaceRoot/.cursor/rules/ssr-first.mdc.template +0 -59
|
@@ -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-jk9q6pse.js";
|
|
11
|
+
import"./index-jtm86byd.js";
|
|
12
|
+
import"./index-6pz1j0zj.js";
|
|
13
13
|
import"./index-67546d0j.js";
|
|
14
14
|
import"./index-r24hmh0q.js";
|
|
15
15
|
|
|
@@ -18,369 +18,7 @@ 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
|
|
24
21
|
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
|
|
384
22
|
var MAX_FILE_LINES = 2000;
|
|
385
23
|
var PLACEHOLDER_EXPORT_NAMES = new Set([
|
|
386
24
|
"aa",
|
|
@@ -463,13 +101,7 @@ var RULE_FIXES = {
|
|
|
463
101
|
"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.",
|
|
464
102
|
"akan.layout.app-root-file": "Move the file into a conventional app folder (common, env, lib, page, private, public, script, srvkit, ui, or webkit).",
|
|
465
103
|
"akan.layout.lib-root-file": "Move the file into a domain module folder under lib/; keep lib root limited to generated support facets.",
|
|
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}`."
|
|
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."
|
|
473
105
|
};
|
|
474
106
|
function getRuleFix(rule) {
|
|
475
107
|
if (rule.startsWith("akan.convention"))
|
|
@@ -482,21 +114,18 @@ class AkanQualityScanner {
|
|
|
482
114
|
const targetFiles = await this.#collectTargetFiles(workspaceRoot);
|
|
483
115
|
const sourceFiles = await Promise.all(targetFiles.filter((file) => !AbstractDoc.isAbstractPath(file)).map((file) => this.#readSourceFile(workspaceRoot, file)));
|
|
484
116
|
const abstractFiles = await Promise.all(targetFiles.filter((file) => AbstractDoc.isAbstractPath(file)).map((file) => this.#readTextFile(workspaceRoot, file)));
|
|
485
|
-
const ssr = new SsrScanner().scan(sourceFiles);
|
|
486
117
|
const warnings = [
|
|
487
118
|
...this.#scanGlobalQuality(sourceFiles),
|
|
488
119
|
...sourceFiles.flatMap((sourceFile) => this.#scanSingleFileQuality(sourceFile)),
|
|
489
120
|
...sourceFiles.flatMap((sourceFile) => this.#scanComponentQuality(sourceFile)),
|
|
490
121
|
...sourceFiles.flatMap((sourceFile) => this.#scanConventionQuality(sourceFile)),
|
|
491
122
|
...sourceFiles.flatMap((sourceFile) => this.#scanLayoutQuality(sourceFile)),
|
|
492
|
-
...abstractFiles.flatMap((abstractFile) => this.#scanAbstractQuality(abstractFile))
|
|
493
|
-
...ssr.warnings
|
|
123
|
+
...abstractFiles.flatMap((abstractFile) => this.#scanAbstractQuality(abstractFile))
|
|
494
124
|
];
|
|
495
125
|
return {
|
|
496
126
|
workspaceRoot,
|
|
497
127
|
scannedFiles: sourceFiles.length + abstractFiles.length,
|
|
498
128
|
warnings: warnings.map((warning) => ({ ...warning, fix: warning.fix ?? getRuleFix(warning.rule) })).sort(compareWarnings),
|
|
499
|
-
ssrBalance: ssr.balance,
|
|
500
129
|
suggestedRules: SUGGESTED_RULES
|
|
501
130
|
};
|
|
502
131
|
}
|
|
@@ -542,7 +171,7 @@ class AkanQualityScanner {
|
|
|
542
171
|
file,
|
|
543
172
|
absolutePath,
|
|
544
173
|
content,
|
|
545
|
-
sourceFile:
|
|
174
|
+
sourceFile: ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true, getScriptKind(file))
|
|
546
175
|
};
|
|
547
176
|
}
|
|
548
177
|
async#readTextFile(workspaceRoot, file) {
|
|
@@ -739,10 +368,6 @@ function formatQualityScanResult(result) {
|
|
|
739
368
|
"",
|
|
740
369
|
...formatQualityWarnings(result.warnings),
|
|
741
370
|
"",
|
|
742
|
-
"SSR balance (component files, JSX elements rendered per side):",
|
|
743
|
-
"",
|
|
744
|
-
...formatSsrBalance(result.ssrBalance),
|
|
745
|
-
"",
|
|
746
371
|
"Suggested quality rules:",
|
|
747
372
|
"",
|
|
748
373
|
...result.suggestedRules.map((rule) => ` - ${rule}`)
|
|
@@ -750,24 +375,6 @@ function formatQualityScanResult(result) {
|
|
|
750
375
|
return sections.join(`
|
|
751
376
|
`);
|
|
752
377
|
}
|
|
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
|
-
}
|
|
771
378
|
function formatQualityWarnings(warnings) {
|
|
772
379
|
if (warnings.length === 0)
|
|
773
380
|
return ["No warnings found."];
|
|
@@ -789,7 +396,7 @@ function getExportedFunctionLikes(sourceFile) {
|
|
|
789
396
|
const declarations = [];
|
|
790
397
|
const nameExempt = isPageRouteFile(sourceFile.file) || isUiComponentFile(sourceFile.file);
|
|
791
398
|
for (const statement of sourceFile.sourceFile.statements) {
|
|
792
|
-
if (
|
|
399
|
+
if (ts.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
|
|
793
400
|
declarations.push({
|
|
794
401
|
name: statement.name.text,
|
|
795
402
|
kind: "function",
|
|
@@ -799,7 +406,7 @@ function getExportedFunctionLikes(sourceFile) {
|
|
|
799
406
|
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false)
|
|
800
407
|
});
|
|
801
408
|
}
|
|
802
|
-
if (
|
|
409
|
+
if (ts.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
803
410
|
declarations.push({
|
|
804
411
|
name: statement.name.text,
|
|
805
412
|
kind: "class",
|
|
@@ -809,9 +416,9 @@ function getExportedFunctionLikes(sourceFile) {
|
|
|
809
416
|
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, isEnumClassStatement(sourceFile.sourceFile, statement))
|
|
810
417
|
});
|
|
811
418
|
}
|
|
812
|
-
if (
|
|
419
|
+
if (ts.isVariableStatement(statement) && isExported(statement)) {
|
|
813
420
|
for (const declaration of statement.declarationList.declarations) {
|
|
814
|
-
if (!
|
|
421
|
+
if (!ts.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
|
|
815
422
|
continue;
|
|
816
423
|
declarations.push({
|
|
817
424
|
name: declaration.name.text,
|
|
@@ -850,14 +457,14 @@ function isInLibModule(file) {
|
|
|
850
457
|
return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
|
|
851
458
|
}
|
|
852
459
|
function isEnumClassStatement(sourceFile, statement) {
|
|
853
|
-
if (!
|
|
460
|
+
if (!ts.isClassDeclaration(statement))
|
|
854
461
|
return false;
|
|
855
|
-
const heritageClause = statement.heritageClauses?.find((clause) => clause.token ===
|
|
462
|
+
const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword);
|
|
856
463
|
const expression = heritageClause?.types[0]?.expression;
|
|
857
464
|
return !!expression && expression.getText(sourceFile).startsWith("enumOf(");
|
|
858
465
|
}
|
|
859
466
|
function getExportedClassNames(sourceFile) {
|
|
860
|
-
return sourceFile.statements.filter((statement) =>
|
|
467
|
+
return sourceFile.statements.filter((statement) => ts.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
|
|
861
468
|
}
|
|
862
469
|
function isComponentDeclarationFile(file) {
|
|
863
470
|
if (!file.endsWith(".tsx"))
|
|
@@ -873,7 +480,7 @@ function isComponentDeclarationFile(file) {
|
|
|
873
480
|
function getComponentFileDeclarations(sourceFile) {
|
|
874
481
|
const reExportedNames = new Set;
|
|
875
482
|
for (const statement of sourceFile.statements) {
|
|
876
|
-
if (
|
|
483
|
+
if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)) {
|
|
877
484
|
for (const element of statement.exportClause.elements)
|
|
878
485
|
reExportedNames.add((element.propertyName ?? element.name).text);
|
|
879
486
|
}
|
|
@@ -883,19 +490,19 @@ function getComponentFileDeclarations(sourceFile) {
|
|
|
883
490
|
const isDefaultExport = isDefaultExportStatement(statement);
|
|
884
491
|
const inlineExported = isExported(statement);
|
|
885
492
|
const add = (name, kind, line) => declarations.push({ name, kind, line, exported: inlineExported || reExportedNames.has(name), isDefaultExport });
|
|
886
|
-
if (
|
|
493
|
+
if (ts.isInterfaceDeclaration(statement))
|
|
887
494
|
add(statement.name.text, "interface", getLine(sourceFile, statement));
|
|
888
|
-
else if (
|
|
495
|
+
else if (ts.isTypeAliasDeclaration(statement))
|
|
889
496
|
add(statement.name.text, "type", getLine(sourceFile, statement));
|
|
890
|
-
else if (
|
|
497
|
+
else if (ts.isEnumDeclaration(statement))
|
|
891
498
|
add(statement.name.text, "enum", getLine(sourceFile, statement));
|
|
892
|
-
else if (
|
|
499
|
+
else if (ts.isFunctionDeclaration(statement) && statement.name)
|
|
893
500
|
add(statement.name.text, "function", getLine(sourceFile, statement));
|
|
894
|
-
else if (
|
|
501
|
+
else if (ts.isClassDeclaration(statement) && statement.name)
|
|
895
502
|
add(statement.name.text, "class", getLine(sourceFile, statement));
|
|
896
|
-
else if (
|
|
503
|
+
else if (ts.isVariableStatement(statement)) {
|
|
897
504
|
for (const declaration of statement.declarationList.declarations) {
|
|
898
|
-
if (!
|
|
505
|
+
if (!ts.isIdentifier(declaration.name))
|
|
899
506
|
continue;
|
|
900
507
|
const kind = isFunctionLikeInitializer(declaration.initializer) ? "function" : "variable";
|
|
901
508
|
add(declaration.name.text, kind, getLine(sourceFile, declaration));
|
|
@@ -907,15 +514,15 @@ function getComponentFileDeclarations(sourceFile) {
|
|
|
907
514
|
function getCompoundComponentNames(sourceFile) {
|
|
908
515
|
const names = new Set;
|
|
909
516
|
for (const statement of sourceFile.statements) {
|
|
910
|
-
if (!
|
|
517
|
+
if (!ts.isExpressionStatement(statement))
|
|
911
518
|
continue;
|
|
912
519
|
const { expression } = statement;
|
|
913
|
-
if (!
|
|
520
|
+
if (!ts.isBinaryExpression(expression) || expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken)
|
|
914
521
|
continue;
|
|
915
|
-
if (!
|
|
522
|
+
if (!ts.isPropertyAccessExpression(expression.left) || !isPascalCaseName(expression.left.name.text))
|
|
916
523
|
continue;
|
|
917
524
|
names.add(expression.left.name.text);
|
|
918
|
-
if (
|
|
525
|
+
if (ts.isIdentifier(expression.right) && isPascalCaseName(expression.right.text))
|
|
919
526
|
names.add(expression.right.text);
|
|
920
527
|
}
|
|
921
528
|
return names;
|
|
@@ -935,9 +542,9 @@ function isPascalCaseName(name) {
|
|
|
935
542
|
return /^[A-Z]/.test(name) && !/^[A-Z0-9_]+$/.test(name);
|
|
936
543
|
}
|
|
937
544
|
function isDefaultExportStatement(statement) {
|
|
938
|
-
if (
|
|
545
|
+
if (ts.isExportAssignment(statement))
|
|
939
546
|
return !statement.isExportEquals;
|
|
940
|
-
return !!(
|
|
547
|
+
return !!(ts.getCombinedModifierFlags(statement) & ts.ModifierFlags.Default);
|
|
941
548
|
}
|
|
942
549
|
function getPlaceholderExportWarnings(sourceFile) {
|
|
943
550
|
if (!sourceFile.file.endsWith("/index.ts") && !sourceFile.file.endsWith("/index.tsx"))
|
|
@@ -967,7 +574,7 @@ function getDictionaryTextWarnings(sourceFile) {
|
|
|
967
574
|
function getGlobalMutationWarnings(sourceFile) {
|
|
968
575
|
const warnings = [];
|
|
969
576
|
for (const statement of sourceFile.sourceFile.statements) {
|
|
970
|
-
if (
|
|
577
|
+
if (ts.isModuleDeclaration(statement) && statement.name.getText(sourceFile.sourceFile) === "global") {
|
|
971
578
|
warnings.push({
|
|
972
579
|
rule: "akan.file.global-declaration",
|
|
973
580
|
scope: "file",
|
|
@@ -977,7 +584,7 @@ function getGlobalMutationWarnings(sourceFile) {
|
|
|
977
584
|
message: "Global declarations require an explicit low-level integration allowlist."
|
|
978
585
|
});
|
|
979
586
|
}
|
|
980
|
-
if (
|
|
587
|
+
if (ts.isInterfaceDeclaration(statement) && statement.name.text === "Window") {
|
|
981
588
|
warnings.push({
|
|
982
589
|
rule: "akan.file.window-augmentation",
|
|
983
590
|
scope: "file",
|
|
@@ -987,7 +594,7 @@ function getGlobalMutationWarnings(sourceFile) {
|
|
|
987
594
|
message: "Window augmentation should be isolated to approved browser integration files."
|
|
988
595
|
});
|
|
989
596
|
}
|
|
990
|
-
if (
|
|
597
|
+
if (ts.isExpressionStatement(statement) && statement.expression.getText(sourceFile.sourceFile).includes(".prototype.")) {
|
|
991
598
|
warnings.push({
|
|
992
599
|
rule: "akan.file.prototype-mutation",
|
|
993
600
|
scope: "file",
|
|
@@ -1005,23 +612,23 @@ function getTopLevelDeclarations(sourceFile) {
|
|
|
1005
612
|
}
|
|
1006
613
|
function getTopLevelDeclaration(sourceFile, statement) {
|
|
1007
614
|
const line = getLine(sourceFile, statement);
|
|
1008
|
-
if (
|
|
615
|
+
if (ts.isClassDeclaration(statement) && statement.name) {
|
|
1009
616
|
return [{ name: statement.name.text, kind: "class", line, exported: isExported(statement), node: statement }];
|
|
1010
617
|
}
|
|
1011
|
-
if (
|
|
618
|
+
if (ts.isFunctionDeclaration(statement) && statement.name) {
|
|
1012
619
|
return [{ name: statement.name.text, kind: "function", line, exported: isExported(statement), node: statement }];
|
|
1013
620
|
}
|
|
1014
|
-
if (
|
|
621
|
+
if (ts.isInterfaceDeclaration(statement)) {
|
|
1015
622
|
return [{ name: statement.name.text, kind: "interface", line, exported: isExported(statement), node: statement }];
|
|
1016
623
|
}
|
|
1017
|
-
if (
|
|
624
|
+
if (ts.isTypeAliasDeclaration(statement)) {
|
|
1018
625
|
return [{ name: statement.name.text, kind: "type", line, exported: isExported(statement), node: statement }];
|
|
1019
626
|
}
|
|
1020
|
-
if (
|
|
627
|
+
if (ts.isEnumDeclaration(statement)) {
|
|
1021
628
|
return [{ name: statement.name.text, kind: "enum", line, exported: isExported(statement), node: statement }];
|
|
1022
629
|
}
|
|
1023
|
-
if (
|
|
1024
|
-
return statement.declarationList.declarations.filter((declaration) =>
|
|
630
|
+
if (ts.isVariableStatement(statement)) {
|
|
631
|
+
return statement.declarationList.declarations.filter((declaration) => ts.isIdentifier(declaration.name)).map((declaration) => ({
|
|
1025
632
|
name: declaration.name.text,
|
|
1026
633
|
kind: "variable",
|
|
1027
634
|
line: getLine(sourceFile, declaration),
|
|
@@ -1029,7 +636,7 @@ function getTopLevelDeclaration(sourceFile, statement) {
|
|
|
1029
636
|
node: statement
|
|
1030
637
|
}));
|
|
1031
638
|
}
|
|
1032
|
-
if (
|
|
639
|
+
if (ts.isExportDeclaration(statement)) {
|
|
1033
640
|
return [{ name: "export declaration", kind: "export", line, exported: true, node: statement }];
|
|
1034
641
|
}
|
|
1035
642
|
return [];
|
|
@@ -1054,9 +661,9 @@ function isAllowedConstantDeclaration(modelName, declaration) {
|
|
|
1054
661
|
return false;
|
|
1055
662
|
if ([`${modelName}Input`, `${modelName}Object`, modelName, `Light${modelName}`, `${modelName}Insight`].includes(declaration.name))
|
|
1056
663
|
return true;
|
|
1057
|
-
if (!
|
|
664
|
+
if (!ts.isClassDeclaration(declaration.node))
|
|
1058
665
|
return false;
|
|
1059
|
-
const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token ===
|
|
666
|
+
const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token === ts.SyntaxKind.ExtendsKeyword);
|
|
1060
667
|
const expression = heritageClause?.types[0]?.expression;
|
|
1061
668
|
return !!expression && expression.getText().startsWith("enumOf(");
|
|
1062
669
|
}
|
|
@@ -1122,13 +729,13 @@ function getModuleInfo(file) {
|
|
|
1122
729
|
return { moduleName, fileName, kind: "database" };
|
|
1123
730
|
}
|
|
1124
731
|
function isExportedConst(declaration) {
|
|
1125
|
-
return declaration.exported &&
|
|
732
|
+
return declaration.exported && ts.isVariableStatement(declaration.node) && (declaration.node.declarationList.flags & ts.NodeFlags.Const) !== 0;
|
|
1126
733
|
}
|
|
1127
734
|
function isExported(node) {
|
|
1128
|
-
return !!
|
|
735
|
+
return !!ts.getCombinedModifierFlags(node) && !!(ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export);
|
|
1129
736
|
}
|
|
1130
737
|
function isFunctionLikeInitializer(node) {
|
|
1131
|
-
return !!node && (
|
|
738
|
+
return !!node && (ts.isArrowFunction(node) || ts.isFunctionExpression(node));
|
|
1132
739
|
}
|
|
1133
740
|
function getBodyFingerprint(sourceFile, node) {
|
|
1134
741
|
if (!node)
|
|
@@ -1150,7 +757,7 @@ async function isDirectory(absolutePath) {
|
|
|
1150
757
|
}
|
|
1151
758
|
}
|
|
1152
759
|
function getScriptKind(file) {
|
|
1153
|
-
return file.endsWith(".tsx") ?
|
|
760
|
+
return file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS;
|
|
1154
761
|
}
|
|
1155
762
|
function getLine(sourceFile, node) {
|
|
1156
763
|
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
@@ -1203,13 +810,6 @@ class QualityScript extends script("quality", [QualityRunner]) {
|
|
|
1203
810
|
spinner.succeed(`Quality scan completed (${result.scannedFiles} files, ${result.warnings.length} warnings)`);
|
|
1204
811
|
Logger.rawLog(format === "json" ? JSON.stringify(result, null, 2) : formatQualityScanResult(result));
|
|
1205
812
|
}
|
|
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
|
-
}
|
|
1213
813
|
}
|
|
1214
814
|
|
|
1215
815
|
// pkgs/@akanjs/cli/quality/quality.command.ts
|
|
@@ -1217,18 +817,15 @@ class QualityCommand extends command("quality", [QualityScript], ({ public: targ
|
|
|
1217
817
|
quality: target({ desc: "Scan apps and libs for Akan code quality warnings" }).arg("action", String, {
|
|
1218
818
|
desc: "quality action",
|
|
1219
819
|
default: "scan",
|
|
1220
|
-
enum: ["scan"
|
|
820
|
+
enum: ["scan"]
|
|
1221
821
|
}).option("format", String, {
|
|
1222
822
|
desc: "output format",
|
|
1223
823
|
default: "text",
|
|
1224
824
|
enum: ["text", "json"]
|
|
1225
825
|
}).with(Workspace).exec(async function(action, format, workspace) {
|
|
1226
|
-
if (action
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
await this.qualityScript.ssr(workspace, format);
|
|
1230
|
-
else
|
|
1231
|
-
throw new Error(`Unknown quality action: ${action}. Use "scan" or "ssr".`);
|
|
826
|
+
if (action !== "scan")
|
|
827
|
+
throw new Error(`Unknown quality action: ${action}. Use "scan".`);
|
|
828
|
+
await this.qualityScript.scan(workspace, format);
|
|
1232
829
|
})
|
|
1233
830
|
})) {
|
|
1234
831
|
}
|