@akanjs/cli 3.0.0-alpha.10 → 3.0.0-alpha.11
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/{agent.command-f0gqkfgz.js → agent.command-a3n15c6t.js} +5 -5
- package/{application.command-x903m2qq.js → application.command-rdm3et0g.js} +4 -4
- package/{applicationBuildRunner-ha3y8ew8.js → applicationBuildRunner-6m8n71w4.js} +1 -1
- package/buildBatch.proc.js +2 -2
- package/{capacitorApp-9xswcgx7.js → capacitorApp-mr81qdxq.js} +2 -2
- package/{cloud.command-zs6kyf83.js → cloud.command-56aeekqn.js} +7 -7
- package/{context.command-fmepgjzg.js → context.command-ysxawe7h.js} +12 -12
- package/{guideline.command-n6sw0a0c.js → guideline.command-yqf9a372.js} +2 -2
- package/guidelines/conventions/conventions.instruction.md +157 -0
- package/guidelines/cssRule/cssRule.instruction.md +7 -0
- package/guidelines/framework/framework.instruction.md +1 -0
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +3 -1
- package/incrementalBuilder.proc.js +1 -1
- package/{index-5sn5w2mt.js → index-3789fdpg.js} +10 -10
- package/{index-y4vkyhvv.js → index-4mgkgba3.js} +2 -2
- package/{index-xmmzf150.js → index-65aeh789.js} +1 -1
- package/{index-d43t8af2.js → index-8pd02prk.js} +2 -2
- package/{index-779ske1y.js → index-9va8zayb.js} +1 -1
- package/{index-82jrh4hj.js → index-a5cs7d8r.js} +5 -5
- package/{index-62955kn5.js → index-bzznxgcx.js} +5 -5
- package/{index-qb61h0k7.js → index-cav1azma.js} +51 -5
- package/{index-qzdjd4ct.js → index-cv9cxs5k.js} +3 -3
- package/{index-774gcx29.js → index-e4b2ae6b.js} +2 -2
- package/{index-kdh76j26.js → index-etwh7sd3.js} +1 -1
- package/{index-gcv8ncfh.js → index-h5whaj26.js} +2 -2
- package/{index-rxkpk7n4.js → index-kcsa1zff.js} +1 -1
- package/{index-3vta1tqa.js → index-m90fn8xe.js} +4 -4
- package/{index-jhpa13zj.js → index-mewacxye.js} +4 -4
- package/{index-4m0bphac.js → index-nwqqssnb.js} +8 -10
- package/{index-jf2ya7r2.js → index-yzmqvg23.js} +3 -3
- package/{index-z8ptfkzv.js → index-yzsxc30b.js} +15 -12
- package/{index-eyyf2149.js → index-z1s5f4e0.js} +3 -3
- package/index.js +18 -18
- package/{library.command-cv9gdbmg.js → library.command-x6t29a30.js} +3 -3
- package/{localRegistry.command-d8fg36zc.js → localRegistry.command-649g1n7b.js} +6 -6
- package/{module.command-qyr1s9cn.js → module.command-8w8ys5t3.js} +5 -5
- package/{package.command-wsjx0k47.js → package.command-zgy7f3fz.js} +3 -3
- package/package.json +2 -2
- package/{page.command-rdzg65rm.js → page.command-yjmnkyk2.js} +3 -3
- package/{primitive.command-89g38b53.js → primitive.command-gfqaagsg.js} +6 -6
- package/{quality.command-0cs2z92d.js → quality.command-ec8r1dn3.js} +361 -90
- package/{repair.command-6fphxfwj.js → repair.command-p9ey6mkm.js} +5 -5
- package/{scalar.command-xfer69s6.js → scalar.command-9dhmqhtm.js} +4 -4
- package/templates/appSample/srvkit/AuthGuard.ts +9 -5
- package/templates/appSample/srvkit/SessionInternalArg.ts +4 -3
- package/templates/workspaceRoot/biome.json.template +1 -0
- package/{workflow.command-2ygfqt4f.js → workflow.command-ycwc6zr6.js} +9 -9
- package/{workspace.command-vbgpjkf9.js → workspace.command-xhyhajxa.js} +50 -33
|
@@ -7,11 +7,11 @@ import {
|
|
|
7
7
|
command,
|
|
8
8
|
runner,
|
|
9
9
|
script
|
|
10
|
-
} from "./index-
|
|
10
|
+
} from "./index-etwh7sd3.js";
|
|
11
11
|
import {
|
|
12
12
|
appRootAllowedFiles,
|
|
13
13
|
libFacetRootAllowedFiles
|
|
14
|
-
} from "./index-
|
|
14
|
+
} from "./index-yzsxc30b.js";
|
|
15
15
|
import"./index-mxvakhsm.js";
|
|
16
16
|
import"./index-1577bej2.js";
|
|
17
17
|
import"./index-67546d0j.js";
|
|
@@ -20,13 +20,163 @@ import"./index-r24hmh0q.js";
|
|
|
20
20
|
// pkgs/@akanjs/devkit/qualityScanner.ts
|
|
21
21
|
import { createHash } from "crypto";
|
|
22
22
|
import { readdir, readFile, stat } from "fs/promises";
|
|
23
|
-
import
|
|
23
|
+
import path3 from "path";
|
|
24
24
|
import { RESERVED_ROUTE_CONFIG_EXPORTS } from "akanjs/common";
|
|
25
25
|
import ignore from "ignore";
|
|
26
|
-
import
|
|
26
|
+
import ts4 from "typescript";
|
|
27
27
|
|
|
28
|
-
// pkgs/@akanjs/devkit/
|
|
28
|
+
// pkgs/@akanjs/devkit/mcpScanner.ts
|
|
29
|
+
import path from "path";
|
|
29
30
|
import ts from "typescript";
|
|
31
|
+
|
|
32
|
+
class McpScanner {
|
|
33
|
+
scan(sourceFiles) {
|
|
34
|
+
const dictionaries = new Map(sourceFiles.filter((sourceFile) => sourceFile.file.endsWith(".dictionary.ts")).map((sourceFile) => [path.dirname(sourceFile.file), sourceFile]));
|
|
35
|
+
return sourceFiles.filter((sourceFile) => sourceFile.file.endsWith(".signal.ts")).flatMap((sourceFile) => this.#scanSignal(sourceFile, dictionaries.get(path.dirname(sourceFile.file))));
|
|
36
|
+
}
|
|
37
|
+
#scanSignal(signal, dictionary) {
|
|
38
|
+
const exposed = McpScanner.#exposedDeclarations(signal);
|
|
39
|
+
if (!exposed.length)
|
|
40
|
+
return [];
|
|
41
|
+
const refName = path.basename(signal.file, ".signal.ts").replace(/^_+/, "");
|
|
42
|
+
const described = dictionary ? McpScanner.#describedEntries(dictionary) : new Map;
|
|
43
|
+
return [
|
|
44
|
+
...exposed.filter(({ name, kind }) => !McpScanner.#isDescribed(described, refName, name, kind)).map(({ name, kind, line }) => ({
|
|
45
|
+
rule: "akan.mcp.missing-description",
|
|
46
|
+
scope: "mcp",
|
|
47
|
+
severity: "warning",
|
|
48
|
+
file: signal.file,
|
|
49
|
+
line,
|
|
50
|
+
message: `MCP-exposed ${kind} "${name}" has no dictionary .desc(); an agent sees its name and nothing else.`
|
|
51
|
+
})),
|
|
52
|
+
...exposed.filter(({ guards }) => guards === "missing").map(({ name, kind, line }) => ({
|
|
53
|
+
rule: "akan.mcp.unguarded-exposure",
|
|
54
|
+
scope: "mcp",
|
|
55
|
+
severity: "warning",
|
|
56
|
+
file: signal.file,
|
|
57
|
+
line,
|
|
58
|
+
message: `MCP-exposed ${kind} "${name}" declares no guards; a slice's guards map never reaches a named slice.`
|
|
59
|
+
}))
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
static #isDescribed(described, refName, name, kind) {
|
|
63
|
+
if (described.get(kind)?.has(name))
|
|
64
|
+
return true;
|
|
65
|
+
return kind === "slice" && !!described.get("endpoint")?.has(`${refName}List${McpScanner.#capitalize(name)}`);
|
|
66
|
+
}
|
|
67
|
+
static #exposedDeclarations(signal) {
|
|
68
|
+
const found = [];
|
|
69
|
+
const visit = (node) => {
|
|
70
|
+
if (McpScanner.#isExposeOption(node)) {
|
|
71
|
+
const declaration = McpScanner.#enclosingDeclaration(node, signal.sourceFile);
|
|
72
|
+
if (declaration)
|
|
73
|
+
found.push(declaration);
|
|
74
|
+
}
|
|
75
|
+
ts.forEachChild(node, visit);
|
|
76
|
+
};
|
|
77
|
+
visit(signal.sourceFile);
|
|
78
|
+
return found;
|
|
79
|
+
}
|
|
80
|
+
static #isExposeOption(node) {
|
|
81
|
+
if (!ts.isPropertyAssignment(node) || McpScanner.#propertyName(node) !== "mcp")
|
|
82
|
+
return false;
|
|
83
|
+
if (!ts.isObjectLiteralExpression(node.initializer))
|
|
84
|
+
return false;
|
|
85
|
+
return node.initializer.properties.some((property) => ts.isPropertyAssignment(property) && McpScanner.#propertyName(property) === "expose" && property.initializer.kind === ts.SyntaxKind.TrueKeyword);
|
|
86
|
+
}
|
|
87
|
+
static #enclosingDeclaration(option, sourceFile) {
|
|
88
|
+
let declaration = null;
|
|
89
|
+
for (let node = option.parent;node; node = node.parent) {
|
|
90
|
+
if (!declaration && ts.isPropertyAssignment(node)) {
|
|
91
|
+
declaration = node;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const kind = McpScanner.#factoryKind(node);
|
|
95
|
+
if (!kind || !declaration)
|
|
96
|
+
continue;
|
|
97
|
+
const name = McpScanner.#propertyName(declaration);
|
|
98
|
+
if (!name)
|
|
99
|
+
return null;
|
|
100
|
+
const line = sourceFile.getLineAndCharacterOfPosition(declaration.getStart(sourceFile)).line + 1;
|
|
101
|
+
return { name, kind, line, guards: McpScanner.#guardState(option) };
|
|
102
|
+
}
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
static #guardState(option) {
|
|
106
|
+
const options = option.parent;
|
|
107
|
+
if (!ts.isObjectLiteralExpression(options))
|
|
108
|
+
return "unknown";
|
|
109
|
+
if (options.properties.some((property) => ts.isSpreadAssignment(property)))
|
|
110
|
+
return "unknown";
|
|
111
|
+
return options.properties.some((property) => ts.isPropertyAssignment(property) && McpScanner.#propertyName(property) === "guards") ? "declared" : "missing";
|
|
112
|
+
}
|
|
113
|
+
static #factoryKind(node) {
|
|
114
|
+
if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression))
|
|
115
|
+
return null;
|
|
116
|
+
const factory = node.expression.text;
|
|
117
|
+
return factory === "slice" || factory === "endpoint" ? factory : null;
|
|
118
|
+
}
|
|
119
|
+
static #describedEntries(dictionary) {
|
|
120
|
+
const described = new Map;
|
|
121
|
+
const visit = (node) => {
|
|
122
|
+
const stage = McpScanner.#dictionaryStage(node);
|
|
123
|
+
if (stage) {
|
|
124
|
+
for (const [name, chain] of McpScanner.#stageEntries(node)) {
|
|
125
|
+
if (!chain.has("desc"))
|
|
126
|
+
continue;
|
|
127
|
+
const names = described.get(stage) ?? new Set;
|
|
128
|
+
names.add(name);
|
|
129
|
+
described.set(stage, names);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
ts.forEachChild(node, visit);
|
|
133
|
+
};
|
|
134
|
+
visit(dictionary.sourceFile);
|
|
135
|
+
return described;
|
|
136
|
+
}
|
|
137
|
+
static #dictionaryStage(node) {
|
|
138
|
+
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression))
|
|
139
|
+
return null;
|
|
140
|
+
const stage = node.expression.name.text;
|
|
141
|
+
return stage === "endpoint" || stage === "slice" ? stage : null;
|
|
142
|
+
}
|
|
143
|
+
static #stageEntries(stage) {
|
|
144
|
+
const callback = stage.arguments[0];
|
|
145
|
+
if (!callback || !ts.isArrowFunction(callback))
|
|
146
|
+
return [];
|
|
147
|
+
const body = ts.isParenthesizedExpression(callback.body) ? callback.body.expression : callback.body;
|
|
148
|
+
if (!ts.isObjectLiteralExpression(body))
|
|
149
|
+
return [];
|
|
150
|
+
return body.properties.flatMap((property) => {
|
|
151
|
+
if (!ts.isPropertyAssignment(property))
|
|
152
|
+
return [];
|
|
153
|
+
const name = McpScanner.#propertyName(property);
|
|
154
|
+
return name ? [[name, McpScanner.#chainCalls(property.initializer)]] : [];
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
static #chainCalls(expression) {
|
|
158
|
+
const calls = new Set;
|
|
159
|
+
let current = expression;
|
|
160
|
+
while (ts.isCallExpression(current) || ts.isPropertyAccessExpression(current)) {
|
|
161
|
+
if (ts.isPropertyAccessExpression(current))
|
|
162
|
+
calls.add(current.name.text);
|
|
163
|
+
current = current.expression;
|
|
164
|
+
}
|
|
165
|
+
return calls;
|
|
166
|
+
}
|
|
167
|
+
static #propertyName(property) {
|
|
168
|
+
const { name } = property;
|
|
169
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name))
|
|
170
|
+
return name.text;
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
static #capitalize(value) {
|
|
174
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// pkgs/@akanjs/devkit/ssrScanner.ts
|
|
179
|
+
import ts2 from "typescript";
|
|
30
180
|
var STATIC_COMPONENT_MIN_MASS = 4;
|
|
31
181
|
var MIXED_COMPONENT_MIN_MASS = 10;
|
|
32
182
|
var MIXED_COMPONENT_MAX_TOUCHES = 2;
|
|
@@ -144,31 +294,31 @@ class SsrScanner {
|
|
|
144
294
|
});
|
|
145
295
|
}
|
|
146
296
|
}
|
|
147
|
-
|
|
297
|
+
ts2.forEachChild(node, visit);
|
|
148
298
|
};
|
|
149
|
-
|
|
299
|
+
ts2.forEachChild(sourceFile.sourceFile, visit);
|
|
150
300
|
return warnings;
|
|
151
301
|
}
|
|
152
302
|
#isMountEffect(sourceFile, node) {
|
|
153
|
-
if (!
|
|
303
|
+
if (!ts2.isCallExpression(node))
|
|
154
304
|
return false;
|
|
155
305
|
const callee = node.expression.getText(sourceFile);
|
|
156
306
|
if (callee !== "useEffect" && callee !== "useLayoutEffect")
|
|
157
307
|
return false;
|
|
158
308
|
const deps = node.arguments[1];
|
|
159
|
-
return !!deps &&
|
|
309
|
+
return !!deps && ts2.isArrayLiteralExpression(deps) && deps.elements.length === 0;
|
|
160
310
|
}
|
|
161
311
|
#getLoadCalls(sourceFile, node) {
|
|
162
312
|
const calls = [];
|
|
163
313
|
const visit = (child) => {
|
|
164
|
-
if (
|
|
314
|
+
if (ts2.isCallExpression(child)) {
|
|
165
315
|
const callee = child.expression.getText(sourceFile);
|
|
166
316
|
if (/^fetch\.[a-z]/.test(callee) || /^st\.do\.(init|get|view|load|list|count|insight)[A-Z]/.test(callee))
|
|
167
317
|
calls.push({ callee, node: child });
|
|
168
318
|
}
|
|
169
|
-
|
|
319
|
+
ts2.forEachChild(child, visit);
|
|
170
320
|
};
|
|
171
|
-
|
|
321
|
+
ts2.forEachChild(node, visit);
|
|
172
322
|
return calls;
|
|
173
323
|
}
|
|
174
324
|
#getTemplateStateWarnings(sourceFile) {
|
|
@@ -176,7 +326,7 @@ class SsrScanner {
|
|
|
176
326
|
return [];
|
|
177
327
|
const warnings = [];
|
|
178
328
|
const visit = (node) => {
|
|
179
|
-
if (
|
|
329
|
+
if (ts2.isCallExpression(node) && node.expression.getText(sourceFile.sourceFile) === "useState") {
|
|
180
330
|
warnings.push({
|
|
181
331
|
rule: "akan.ssr.template-client-state",
|
|
182
332
|
scope: "ssr",
|
|
@@ -186,9 +336,9 @@ class SsrScanner {
|
|
|
186
336
|
message: "Template holds form state in useState. Templates are store-driven and carry no local state."
|
|
187
337
|
});
|
|
188
338
|
}
|
|
189
|
-
|
|
339
|
+
ts2.forEachChild(node, visit);
|
|
190
340
|
};
|
|
191
|
-
|
|
341
|
+
ts2.forEachChild(sourceFile.sourceFile, visit);
|
|
192
342
|
return warnings;
|
|
193
343
|
}
|
|
194
344
|
#measureBalance(sourceFiles) {
|
|
@@ -229,14 +379,14 @@ class SsrScanner {
|
|
|
229
379
|
return components;
|
|
230
380
|
}
|
|
231
381
|
#getComponentNodes(statement) {
|
|
232
|
-
if (
|
|
382
|
+
if (ts2.isFunctionDeclaration(statement) && statement.body)
|
|
233
383
|
return [{ name: statement.name?.text ?? "default", node: statement.body }];
|
|
234
|
-
if (!
|
|
384
|
+
if (!ts2.isVariableStatement(statement))
|
|
235
385
|
return [];
|
|
236
386
|
return statement.declarationList.declarations.flatMap((declaration) => {
|
|
237
|
-
if (!
|
|
387
|
+
if (!ts2.isIdentifier(declaration.name) || !declaration.initializer)
|
|
238
388
|
return [];
|
|
239
|
-
if (!
|
|
389
|
+
if (!ts2.isArrowFunction(declaration.initializer) && !ts2.isFunctionExpression(declaration.initializer))
|
|
240
390
|
return [];
|
|
241
391
|
return [{ name: declaration.name.text, node: declaration.initializer }];
|
|
242
392
|
});
|
|
@@ -244,7 +394,7 @@ class SsrScanner {
|
|
|
244
394
|
#getTouches(sourceFile, node) {
|
|
245
395
|
const touches = [];
|
|
246
396
|
const visit = (child) => {
|
|
247
|
-
if (
|
|
397
|
+
if (ts2.isCallExpression(child)) {
|
|
248
398
|
const callee = child.expression.getText(sourceFile);
|
|
249
399
|
const bareName = callee.split(".").pop() ?? callee;
|
|
250
400
|
if (callee === "createContext" || callee === "lazy")
|
|
@@ -252,60 +402,60 @@ class SsrScanner {
|
|
|
252
402
|
else if (/^use[A-Z]/.test(bareName) && !SsrScanner.#serverSafeCalls.has(bareName))
|
|
253
403
|
touches.push(bareName);
|
|
254
404
|
}
|
|
255
|
-
if (
|
|
405
|
+
if (ts2.isJsxAttribute(child) && /^on[A-Z]/.test(child.name.getText(sourceFile)))
|
|
256
406
|
touches.push(child.name.getText(sourceFile));
|
|
257
|
-
if (
|
|
407
|
+
if (ts2.isPropertyAccessExpression(child)) {
|
|
258
408
|
const root = getAccessRoot(child);
|
|
259
409
|
if (root === "st")
|
|
260
410
|
touches.push("st");
|
|
261
411
|
else if (SsrScanner.#clientGlobals.has(root))
|
|
262
412
|
touches.push(root);
|
|
263
413
|
}
|
|
264
|
-
|
|
414
|
+
ts2.forEachChild(child, visit);
|
|
265
415
|
};
|
|
266
|
-
|
|
416
|
+
ts2.forEachChild(node, visit);
|
|
267
417
|
return touches;
|
|
268
418
|
}
|
|
269
419
|
#getTagNames(sourceFile, node) {
|
|
270
420
|
const tags = new Set;
|
|
271
421
|
const visit = (child) => {
|
|
272
|
-
if (
|
|
422
|
+
if (ts2.isJsxOpeningElement(child) || ts2.isJsxSelfClosingElement(child))
|
|
273
423
|
tags.add(child.tagName.getText(sourceFile).split(".")[0]);
|
|
274
|
-
|
|
424
|
+
ts2.forEachChild(child, visit);
|
|
275
425
|
};
|
|
276
|
-
|
|
426
|
+
ts2.forEachChild(node, visit);
|
|
277
427
|
return tags;
|
|
278
428
|
}
|
|
279
429
|
#getMass(node) {
|
|
280
430
|
let mass = 0;
|
|
281
431
|
const visit = (child) => {
|
|
282
|
-
if (
|
|
432
|
+
if (ts2.isJsxOpeningElement(child) || ts2.isJsxSelfClosingElement(child))
|
|
283
433
|
mass += 1;
|
|
284
|
-
|
|
434
|
+
ts2.forEachChild(child, visit);
|
|
285
435
|
};
|
|
286
|
-
|
|
436
|
+
ts2.forEachChild(node, visit);
|
|
287
437
|
return mass;
|
|
288
438
|
}
|
|
289
439
|
#hasUseClient(sourceFile) {
|
|
290
440
|
const first = sourceFile.statements[0];
|
|
291
|
-
if (!first || !
|
|
441
|
+
if (!first || !ts2.isExpressionStatement(first) || !ts2.isStringLiteral(first.expression))
|
|
292
442
|
return false;
|
|
293
443
|
return first.expression.text === "use client";
|
|
294
444
|
}
|
|
295
445
|
#hasVendorImport(sourceFile) {
|
|
296
|
-
return sourceFile.statements.some((statement) =>
|
|
446
|
+
return sourceFile.statements.some((statement) => ts2.isImportDeclaration(statement) && isVendorSpecifier(getSpecifier(statement)));
|
|
297
447
|
}
|
|
298
448
|
#getVendorNames(sourceFile) {
|
|
299
449
|
const names = new Set;
|
|
300
450
|
for (const statement of sourceFile.statements) {
|
|
301
|
-
if (!
|
|
451
|
+
if (!ts2.isImportDeclaration(statement) || !isVendorSpecifier(getSpecifier(statement)))
|
|
302
452
|
continue;
|
|
303
453
|
const clause = statement.importClause;
|
|
304
454
|
if (clause?.name)
|
|
305
455
|
names.add(clause.name.text);
|
|
306
|
-
if (clause?.namedBindings &&
|
|
456
|
+
if (clause?.namedBindings && ts2.isNamespaceImport(clause.namedBindings))
|
|
307
457
|
names.add(clause.namedBindings.name.text);
|
|
308
|
-
if (clause?.namedBindings &&
|
|
458
|
+
if (clause?.namedBindings && ts2.isNamedImports(clause.namedBindings))
|
|
309
459
|
for (const element of clause.namedBindings.elements)
|
|
310
460
|
names.add(element.name.text);
|
|
311
461
|
}
|
|
@@ -313,10 +463,10 @@ class SsrScanner {
|
|
|
313
463
|
}
|
|
314
464
|
#importsClientRuntime(sourceFile) {
|
|
315
465
|
for (const statement of sourceFile.statements) {
|
|
316
|
-
if (!
|
|
466
|
+
if (!ts2.isImportDeclaration(statement))
|
|
317
467
|
continue;
|
|
318
468
|
const bindings = statement.importClause?.namedBindings;
|
|
319
|
-
if (!bindings || !
|
|
469
|
+
if (!bindings || !ts2.isNamedImports(bindings))
|
|
320
470
|
continue;
|
|
321
471
|
if (bindings.elements.some((element) => SsrScanner.#clientRuntimeImports.has(element.name.text)))
|
|
322
472
|
return true;
|
|
@@ -367,7 +517,7 @@ function getShare(serverMass, clientMass) {
|
|
|
367
517
|
return total === 0 ? 1 : serverMass / total;
|
|
368
518
|
}
|
|
369
519
|
function getSpecifier(statement) {
|
|
370
|
-
return
|
|
520
|
+
return ts2.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : "";
|
|
371
521
|
}
|
|
372
522
|
function isVendorSpecifier(specifier) {
|
|
373
523
|
if (specifier === "" || specifier.startsWith(".") || specifier.startsWith("/"))
|
|
@@ -380,9 +530,125 @@ function isVendorSpecifier(specifier) {
|
|
|
380
530
|
}
|
|
381
531
|
function getAccessRoot(node) {
|
|
382
532
|
let current = node;
|
|
383
|
-
while (
|
|
533
|
+
while (ts2.isPropertyAccessExpression(current))
|
|
384
534
|
current = current.expression;
|
|
385
|
-
return
|
|
535
|
+
return ts2.isIdentifier(current) ? current.text : "";
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// pkgs/@akanjs/devkit/storeScanner.ts
|
|
539
|
+
import path2 from "path";
|
|
540
|
+
import ts3 from "typescript";
|
|
541
|
+
|
|
542
|
+
class StoreScanner {
|
|
543
|
+
scan(sourceFiles) {
|
|
544
|
+
const dictionaries = new Map(sourceFiles.filter((sourceFile) => sourceFile.file.endsWith(".dictionary.ts")).map((sourceFile) => [path2.dirname(sourceFile.file), sourceFile]));
|
|
545
|
+
return sourceFiles.filter((sourceFile) => sourceFile.file.endsWith(".store.ts")).flatMap((sourceFile) => this.#scanStore(sourceFile, dictionaries.get(path2.dirname(sourceFile.file))));
|
|
546
|
+
}
|
|
547
|
+
#scanStore(store, dictionary) {
|
|
548
|
+
const actions = StoreScanner.#customActions(store);
|
|
549
|
+
if (!actions.length)
|
|
550
|
+
return [];
|
|
551
|
+
const described = dictionary ? StoreScanner.#describedEntries(dictionary) : new Map;
|
|
552
|
+
return actions.filter(({ name, fetched }) => fetched.length && !fetched.includes(name)).filter(({ name }) => !described.get("store")?.has(name) && !described.get("endpoint")?.has(name)).map(({ name, line, fetched }) => ({
|
|
553
|
+
rule: "akan.agent.missing-store-description",
|
|
554
|
+
scope: "agent",
|
|
555
|
+
severity: "warning",
|
|
556
|
+
file: store.file,
|
|
557
|
+
line,
|
|
558
|
+
message: `Store action "${name}" calls ${fetched.map((key) => `${key}()`).join(", ")} under a different name and has no dictionary .store() entry, so an agent reading it has the name and nothing else.`
|
|
559
|
+
}));
|
|
560
|
+
}
|
|
561
|
+
static #customActions(store) {
|
|
562
|
+
const actions = [];
|
|
563
|
+
const visit = (node) => {
|
|
564
|
+
if (ts3.isClassDeclaration(node) && StoreScanner.#extendsStore(node)) {
|
|
565
|
+
for (const member of node.members) {
|
|
566
|
+
if (!ts3.isMethodDeclaration(member))
|
|
567
|
+
continue;
|
|
568
|
+
if (member.modifiers?.some((modifier) => modifier.kind === ts3.SyntaxKind.StaticKeyword))
|
|
569
|
+
continue;
|
|
570
|
+
const name = StoreScanner.#memberName(member);
|
|
571
|
+
if (!name)
|
|
572
|
+
continue;
|
|
573
|
+
const line = store.sourceFile.getLineAndCharacterOfPosition(member.getStart(store.sourceFile)).line + 1;
|
|
574
|
+
actions.push({ name, line, fetched: StoreScanner.#fetchedEndpoints(member) });
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
ts3.forEachChild(node, visit);
|
|
578
|
+
};
|
|
579
|
+
visit(store.sourceFile);
|
|
580
|
+
return actions;
|
|
581
|
+
}
|
|
582
|
+
static #extendsStore(node) {
|
|
583
|
+
return !!node.heritageClauses?.some((clause) => clause.types.some((type) => ts3.isCallExpression(type.expression) && StoreScanner.#isStoreCall(type.expression)));
|
|
584
|
+
}
|
|
585
|
+
static #isStoreCall(expression) {
|
|
586
|
+
return ts3.isIdentifier(expression.expression) && expression.expression.text === "store";
|
|
587
|
+
}
|
|
588
|
+
static #fetchedEndpoints(member) {
|
|
589
|
+
const fetched = new Set;
|
|
590
|
+
const visit = (node) => {
|
|
591
|
+
if (ts3.isCallExpression(node) && ts3.isPropertyAccessExpression(node.expression) && ts3.isIdentifier(node.expression.expression) && node.expression.expression.text === "fetch")
|
|
592
|
+
fetched.add(node.expression.name.text);
|
|
593
|
+
ts3.forEachChild(node, visit);
|
|
594
|
+
};
|
|
595
|
+
visit(member);
|
|
596
|
+
return [...fetched];
|
|
597
|
+
}
|
|
598
|
+
static #describedEntries(dictionary) {
|
|
599
|
+
const described = new Map;
|
|
600
|
+
const visit = (node) => {
|
|
601
|
+
const stage = StoreScanner.#dictionaryStage(node);
|
|
602
|
+
if (stage) {
|
|
603
|
+
for (const [name, chain] of StoreScanner.#stageEntries(node)) {
|
|
604
|
+
if (!chain.has("desc"))
|
|
605
|
+
continue;
|
|
606
|
+
const names = described.get(stage) ?? new Set;
|
|
607
|
+
names.add(name);
|
|
608
|
+
described.set(stage, names);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
ts3.forEachChild(node, visit);
|
|
612
|
+
};
|
|
613
|
+
visit(dictionary.sourceFile);
|
|
614
|
+
return described;
|
|
615
|
+
}
|
|
616
|
+
static #dictionaryStage(node) {
|
|
617
|
+
if (!ts3.isCallExpression(node) || !ts3.isPropertyAccessExpression(node.expression))
|
|
618
|
+
return null;
|
|
619
|
+
const stage = node.expression.name.text;
|
|
620
|
+
return stage === "store" || stage === "endpoint" ? stage : null;
|
|
621
|
+
}
|
|
622
|
+
static #stageEntries(stage) {
|
|
623
|
+
const callback = stage.arguments[0];
|
|
624
|
+
if (!callback || !ts3.isArrowFunction(callback))
|
|
625
|
+
return [];
|
|
626
|
+
const body = ts3.isParenthesizedExpression(callback.body) ? callback.body.expression : callback.body;
|
|
627
|
+
if (!ts3.isObjectLiteralExpression(body))
|
|
628
|
+
return [];
|
|
629
|
+
return body.properties.flatMap((property) => {
|
|
630
|
+
if (!ts3.isPropertyAssignment(property))
|
|
631
|
+
return [];
|
|
632
|
+
const name = StoreScanner.#memberName(property);
|
|
633
|
+
return name ? [[name, StoreScanner.#chainCalls(property.initializer)]] : [];
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
static #chainCalls(expression) {
|
|
637
|
+
const calls = new Set;
|
|
638
|
+
let current = expression;
|
|
639
|
+
while (ts3.isCallExpression(current) || ts3.isPropertyAccessExpression(current)) {
|
|
640
|
+
if (ts3.isPropertyAccessExpression(current))
|
|
641
|
+
calls.add(current.name.text);
|
|
642
|
+
current = current.expression;
|
|
643
|
+
}
|
|
644
|
+
return calls;
|
|
645
|
+
}
|
|
646
|
+
static #memberName(member) {
|
|
647
|
+
const { name } = member;
|
|
648
|
+
if (!name || !ts3.isIdentifier(name) && !ts3.isStringLiteral(name))
|
|
649
|
+
return null;
|
|
650
|
+
return name.text;
|
|
651
|
+
}
|
|
386
652
|
}
|
|
387
653
|
|
|
388
654
|
// pkgs/@akanjs/devkit/qualityScanner.ts
|
|
@@ -439,7 +705,10 @@ var RULE_FIXES = {
|
|
|
439
705
|
"akan.ssr.client-static-markup": "Keep the interactive element in the client component and hoist the static subtree into a server component, then accept it as `children` or render it through a Unit/View reference.",
|
|
440
706
|
"akan.ssr.client-mount-load": "Load the data in the route with `fetch.initX(...)` / `fetch.viewX(...)` and pass the init/view object down as a prop; the client store hydrates from it and the effect goes away.",
|
|
441
707
|
"akan.ssr.module-missing-server-view": "Add a <Model>.Unit.tsx for list/card rendering and a <Model>.View.tsx for the detail surface, then have the Zone delegate to them.",
|
|
442
|
-
"akan.ssr.template-client-state": "Bind the field to the store instead: `value={xForm.field}` with `onChange={st.do.setFieldOnX}`."
|
|
708
|
+
"akan.ssr.template-client-state": "Bind the field to the store instead: `value={xForm.field}` with `onChange={st.do.setFieldOnX}`.",
|
|
709
|
+
"akan.mcp.missing-description": "Add `.desc([en, ko])` to this entry in the module's dictionary \u2014 for a slice, either on the slice entry or on the `<model>List<Slice>` endpoint it generates. Describe when to reach for it, not what it is named.",
|
|
710
|
+
"akan.agent.missing-store-description": "Add the action to the module dictionary's `.store()` stage with a `.desc([en, ko])` saying what it does for the user \u2014 not what the endpoint it calls does. That stage is optional everywhere else: an action named after its endpoint already reads as that endpoint's description.",
|
|
711
|
+
"akan.mcp.unguarded-exposure": "Name the guards in the same option object as `mcp`: `init({ guards: [SignedIn], mcp: { expose: true } })`. Write `guards: [Public]` if anonymous reads are the intent \u2014 the access is the same, but only one of the two is a decision. The `slice()` call's guards map reaches the root slice and base CRUD, never a named slice."
|
|
443
712
|
};
|
|
444
713
|
function getRuleFix(rule) {
|
|
445
714
|
if (rule.startsWith("akan.convention"))
|
|
@@ -460,7 +729,9 @@ class AkanQualityScanner {
|
|
|
460
729
|
...sourceFiles.flatMap((sourceFile) => this.#scanConventionQuality(sourceFile)),
|
|
461
730
|
...sourceFiles.flatMap((sourceFile) => this.#scanLayoutQuality(sourceFile)),
|
|
462
731
|
...abstractFiles.flatMap((abstractFile) => this.#scanAbstractQuality(abstractFile)),
|
|
463
|
-
...ssr.warnings
|
|
732
|
+
...ssr.warnings,
|
|
733
|
+
...new McpScanner().scan(sourceFiles),
|
|
734
|
+
...new StoreScanner().scan(sourceFiles)
|
|
464
735
|
];
|
|
465
736
|
return {
|
|
466
737
|
workspaceRoot,
|
|
@@ -474,7 +745,7 @@ class AkanQualityScanner {
|
|
|
474
745
|
const ignoreFilter = ignore().add(await this.#readGitIgnore(workspaceRoot));
|
|
475
746
|
const files = [];
|
|
476
747
|
for (const targetRoot of ["apps", "libs"]) {
|
|
477
|
-
const absoluteTargetRoot =
|
|
748
|
+
const absoluteTargetRoot = path3.join(workspaceRoot, targetRoot);
|
|
478
749
|
if (!await isDirectory(absoluteTargetRoot))
|
|
479
750
|
continue;
|
|
480
751
|
await this.#walkTargetFiles(workspaceRoot, absoluteTargetRoot, ignoreFilter, files);
|
|
@@ -482,7 +753,7 @@ class AkanQualityScanner {
|
|
|
482
753
|
return files.sort();
|
|
483
754
|
}
|
|
484
755
|
async#readGitIgnore(workspaceRoot) {
|
|
485
|
-
const gitIgnorePath =
|
|
756
|
+
const gitIgnorePath = path3.join(workspaceRoot, ".gitignore");
|
|
486
757
|
if (!await Bun.file(gitIgnorePath).exists())
|
|
487
758
|
return [];
|
|
488
759
|
return (await readFile(gitIgnorePath, "utf8")).split(/\r?\n/);
|
|
@@ -490,8 +761,8 @@ class AkanQualityScanner {
|
|
|
490
761
|
async#walkTargetFiles(workspaceRoot, currentPath, ignoreFilter, files) {
|
|
491
762
|
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
492
763
|
for (const entry of entries) {
|
|
493
|
-
const absolutePath =
|
|
494
|
-
const relativePath = toPosix(
|
|
764
|
+
const absolutePath = path3.join(currentPath, entry.name);
|
|
765
|
+
const relativePath = toPosix(path3.relative(workspaceRoot, absolutePath));
|
|
495
766
|
if (shouldSkipPath(relativePath, entry.isDirectory(), ignoreFilter))
|
|
496
767
|
continue;
|
|
497
768
|
if (entry.isDirectory()) {
|
|
@@ -506,17 +777,17 @@ class AkanQualityScanner {
|
|
|
506
777
|
}
|
|
507
778
|
}
|
|
508
779
|
async#readSourceFile(workspaceRoot, file) {
|
|
509
|
-
const absolutePath =
|
|
780
|
+
const absolutePath = path3.join(workspaceRoot, file);
|
|
510
781
|
const content = await readFile(absolutePath, "utf8");
|
|
511
782
|
return {
|
|
512
783
|
file,
|
|
513
784
|
absolutePath,
|
|
514
785
|
content,
|
|
515
|
-
sourceFile:
|
|
786
|
+
sourceFile: ts4.createSourceFile(file, content, ts4.ScriptTarget.Latest, true, getScriptKind(file))
|
|
516
787
|
};
|
|
517
788
|
}
|
|
518
789
|
async#readTextFile(workspaceRoot, file) {
|
|
519
|
-
return { file, content: await readFile(
|
|
790
|
+
return { file, content: await readFile(path3.join(workspaceRoot, file), "utf8") };
|
|
520
791
|
}
|
|
521
792
|
#scanGlobalQuality(sourceFiles) {
|
|
522
793
|
const exportedFunctionLikes = sourceFiles.flatMap((sourceFile) => getExportedFunctionLikes(sourceFile));
|
|
@@ -640,7 +911,7 @@ class AkanQualityScanner {
|
|
|
640
911
|
const suffix = CONVENTION_SUFFIXES.find((candidate) => sourceFile.file.endsWith(candidate));
|
|
641
912
|
if (!suffix)
|
|
642
913
|
return [];
|
|
643
|
-
const modelName = toPascalCase(
|
|
914
|
+
const modelName = toPascalCase(path3.basename(sourceFile.file, suffix));
|
|
644
915
|
const warnings = [];
|
|
645
916
|
for (const declaration of getTopLevelDeclarations(sourceFile)) {
|
|
646
917
|
if (isAllowedConventionDeclaration(suffix, modelName, declaration))
|
|
@@ -651,7 +922,7 @@ class AkanQualityScanner {
|
|
|
651
922
|
severity: "warning",
|
|
652
923
|
file: sourceFile.file,
|
|
653
924
|
line: declaration.line,
|
|
654
|
-
message: `${
|
|
925
|
+
message: `${path3.basename(sourceFile.file)} should not declare top-level ${declaration.kind} "${declaration.name}". Allowed declarations: ${getConventionDescription(suffix, modelName)}.`
|
|
655
926
|
});
|
|
656
927
|
}
|
|
657
928
|
return warnings;
|
|
@@ -759,7 +1030,7 @@ function getExportedFunctionLikes(sourceFile) {
|
|
|
759
1030
|
const declarations = [];
|
|
760
1031
|
const nameExempt = isPageRouteFile(sourceFile.file) || isUiComponentFile(sourceFile.file);
|
|
761
1032
|
for (const statement of sourceFile.sourceFile.statements) {
|
|
762
|
-
if (
|
|
1033
|
+
if (ts4.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
|
|
763
1034
|
declarations.push({
|
|
764
1035
|
name: statement.name.text,
|
|
765
1036
|
kind: "function",
|
|
@@ -769,7 +1040,7 @@ function getExportedFunctionLikes(sourceFile) {
|
|
|
769
1040
|
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false)
|
|
770
1041
|
});
|
|
771
1042
|
}
|
|
772
|
-
if (
|
|
1043
|
+
if (ts4.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
773
1044
|
declarations.push({
|
|
774
1045
|
name: statement.name.text,
|
|
775
1046
|
kind: "class",
|
|
@@ -779,9 +1050,9 @@ function getExportedFunctionLikes(sourceFile) {
|
|
|
779
1050
|
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, isEnumClassStatement(sourceFile.sourceFile, statement))
|
|
780
1051
|
});
|
|
781
1052
|
}
|
|
782
|
-
if (
|
|
1053
|
+
if (ts4.isVariableStatement(statement) && isExported(statement)) {
|
|
783
1054
|
for (const declaration of statement.declarationList.declarations) {
|
|
784
|
-
if (!
|
|
1055
|
+
if (!ts4.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
|
|
785
1056
|
continue;
|
|
786
1057
|
declarations.push({
|
|
787
1058
|
name: declaration.name.text,
|
|
@@ -820,14 +1091,14 @@ function isInLibModule(file) {
|
|
|
820
1091
|
return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
|
|
821
1092
|
}
|
|
822
1093
|
function isEnumClassStatement(sourceFile, statement) {
|
|
823
|
-
if (!
|
|
1094
|
+
if (!ts4.isClassDeclaration(statement))
|
|
824
1095
|
return false;
|
|
825
|
-
const heritageClause = statement.heritageClauses?.find((clause) => clause.token ===
|
|
1096
|
+
const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts4.SyntaxKind.ExtendsKeyword);
|
|
826
1097
|
const expression = heritageClause?.types[0]?.expression;
|
|
827
1098
|
return !!expression && expression.getText(sourceFile).startsWith("enumOf(");
|
|
828
1099
|
}
|
|
829
1100
|
function getExportedClassNames(sourceFile) {
|
|
830
|
-
return sourceFile.statements.filter((statement) =>
|
|
1101
|
+
return sourceFile.statements.filter((statement) => ts4.isClassDeclaration(statement) && !!statement.name).filter((statement) => isExported(statement)).map((statement) => statement.name.text);
|
|
831
1102
|
}
|
|
832
1103
|
function isComponentDeclarationFile(file) {
|
|
833
1104
|
if (!file.endsWith(".tsx"))
|
|
@@ -843,7 +1114,7 @@ function isComponentDeclarationFile(file) {
|
|
|
843
1114
|
function getComponentFileDeclarations(sourceFile) {
|
|
844
1115
|
const reExportedNames = new Set;
|
|
845
1116
|
for (const statement of sourceFile.statements) {
|
|
846
|
-
if (
|
|
1117
|
+
if (ts4.isExportDeclaration(statement) && statement.exportClause && ts4.isNamedExports(statement.exportClause)) {
|
|
847
1118
|
for (const element of statement.exportClause.elements)
|
|
848
1119
|
reExportedNames.add((element.propertyName ?? element.name).text);
|
|
849
1120
|
}
|
|
@@ -853,19 +1124,19 @@ function getComponentFileDeclarations(sourceFile) {
|
|
|
853
1124
|
const isDefaultExport = isDefaultExportStatement(statement);
|
|
854
1125
|
const inlineExported = isExported(statement);
|
|
855
1126
|
const add = (name, kind, line) => declarations.push({ name, kind, line, exported: inlineExported || reExportedNames.has(name), isDefaultExport });
|
|
856
|
-
if (
|
|
1127
|
+
if (ts4.isInterfaceDeclaration(statement))
|
|
857
1128
|
add(statement.name.text, "interface", getLine(sourceFile, statement));
|
|
858
|
-
else if (
|
|
1129
|
+
else if (ts4.isTypeAliasDeclaration(statement))
|
|
859
1130
|
add(statement.name.text, "type", getLine(sourceFile, statement));
|
|
860
|
-
else if (
|
|
1131
|
+
else if (ts4.isEnumDeclaration(statement))
|
|
861
1132
|
add(statement.name.text, "enum", getLine(sourceFile, statement));
|
|
862
|
-
else if (
|
|
1133
|
+
else if (ts4.isFunctionDeclaration(statement) && statement.name)
|
|
863
1134
|
add(statement.name.text, "function", getLine(sourceFile, statement));
|
|
864
|
-
else if (
|
|
1135
|
+
else if (ts4.isClassDeclaration(statement) && statement.name)
|
|
865
1136
|
add(statement.name.text, "class", getLine(sourceFile, statement));
|
|
866
|
-
else if (
|
|
1137
|
+
else if (ts4.isVariableStatement(statement)) {
|
|
867
1138
|
for (const declaration of statement.declarationList.declarations) {
|
|
868
|
-
if (!
|
|
1139
|
+
if (!ts4.isIdentifier(declaration.name))
|
|
869
1140
|
continue;
|
|
870
1141
|
const kind = isFunctionLikeInitializer(declaration.initializer) ? "function" : "variable";
|
|
871
1142
|
add(declaration.name.text, kind, getLine(sourceFile, declaration));
|
|
@@ -877,15 +1148,15 @@ function getComponentFileDeclarations(sourceFile) {
|
|
|
877
1148
|
function getCompoundComponentNames(sourceFile) {
|
|
878
1149
|
const names = new Set;
|
|
879
1150
|
for (const statement of sourceFile.statements) {
|
|
880
|
-
if (!
|
|
1151
|
+
if (!ts4.isExpressionStatement(statement))
|
|
881
1152
|
continue;
|
|
882
1153
|
const { expression } = statement;
|
|
883
|
-
if (!
|
|
1154
|
+
if (!ts4.isBinaryExpression(expression) || expression.operatorToken.kind !== ts4.SyntaxKind.EqualsToken)
|
|
884
1155
|
continue;
|
|
885
|
-
if (!
|
|
1156
|
+
if (!ts4.isPropertyAccessExpression(expression.left) || !isPascalCaseName(expression.left.name.text))
|
|
886
1157
|
continue;
|
|
887
1158
|
names.add(expression.left.name.text);
|
|
888
|
-
if (
|
|
1159
|
+
if (ts4.isIdentifier(expression.right) && isPascalCaseName(expression.right.text))
|
|
889
1160
|
names.add(expression.right.text);
|
|
890
1161
|
}
|
|
891
1162
|
return names;
|
|
@@ -905,9 +1176,9 @@ function isPascalCaseName(name) {
|
|
|
905
1176
|
return /^[A-Z]/.test(name) && !/^[A-Z0-9_]+$/.test(name);
|
|
906
1177
|
}
|
|
907
1178
|
function isDefaultExportStatement(statement) {
|
|
908
|
-
if (
|
|
1179
|
+
if (ts4.isExportAssignment(statement))
|
|
909
1180
|
return !statement.isExportEquals;
|
|
910
|
-
return !!(
|
|
1181
|
+
return !!(ts4.getCombinedModifierFlags(statement) & ts4.ModifierFlags.Default);
|
|
911
1182
|
}
|
|
912
1183
|
function getPlaceholderExportWarnings(sourceFile) {
|
|
913
1184
|
if (!sourceFile.file.endsWith("/index.ts") && !sourceFile.file.endsWith("/index.tsx"))
|
|
@@ -937,7 +1208,7 @@ function getDictionaryTextWarnings(sourceFile) {
|
|
|
937
1208
|
function getGlobalMutationWarnings(sourceFile) {
|
|
938
1209
|
const warnings = [];
|
|
939
1210
|
for (const statement of sourceFile.sourceFile.statements) {
|
|
940
|
-
if (
|
|
1211
|
+
if (ts4.isModuleDeclaration(statement) && statement.name.getText(sourceFile.sourceFile) === "global") {
|
|
941
1212
|
warnings.push({
|
|
942
1213
|
rule: "akan.file.global-declaration",
|
|
943
1214
|
scope: "file",
|
|
@@ -947,7 +1218,7 @@ function getGlobalMutationWarnings(sourceFile) {
|
|
|
947
1218
|
message: "Global declarations require an explicit low-level integration allowlist."
|
|
948
1219
|
});
|
|
949
1220
|
}
|
|
950
|
-
if (
|
|
1221
|
+
if (ts4.isInterfaceDeclaration(statement) && statement.name.text === "Window") {
|
|
951
1222
|
warnings.push({
|
|
952
1223
|
rule: "akan.file.window-augmentation",
|
|
953
1224
|
scope: "file",
|
|
@@ -957,7 +1228,7 @@ function getGlobalMutationWarnings(sourceFile) {
|
|
|
957
1228
|
message: "Window augmentation should be isolated to approved browser integration files."
|
|
958
1229
|
});
|
|
959
1230
|
}
|
|
960
|
-
if (
|
|
1231
|
+
if (ts4.isExpressionStatement(statement) && statement.expression.getText(sourceFile.sourceFile).includes(".prototype.")) {
|
|
961
1232
|
warnings.push({
|
|
962
1233
|
rule: "akan.file.prototype-mutation",
|
|
963
1234
|
scope: "file",
|
|
@@ -975,23 +1246,23 @@ function getTopLevelDeclarations(sourceFile) {
|
|
|
975
1246
|
}
|
|
976
1247
|
function getTopLevelDeclaration(sourceFile, statement) {
|
|
977
1248
|
const line = getLine(sourceFile, statement);
|
|
978
|
-
if (
|
|
1249
|
+
if (ts4.isClassDeclaration(statement) && statement.name) {
|
|
979
1250
|
return [{ name: statement.name.text, kind: "class", line, exported: isExported(statement), node: statement }];
|
|
980
1251
|
}
|
|
981
|
-
if (
|
|
1252
|
+
if (ts4.isFunctionDeclaration(statement) && statement.name) {
|
|
982
1253
|
return [{ name: statement.name.text, kind: "function", line, exported: isExported(statement), node: statement }];
|
|
983
1254
|
}
|
|
984
|
-
if (
|
|
1255
|
+
if (ts4.isInterfaceDeclaration(statement)) {
|
|
985
1256
|
return [{ name: statement.name.text, kind: "interface", line, exported: isExported(statement), node: statement }];
|
|
986
1257
|
}
|
|
987
|
-
if (
|
|
1258
|
+
if (ts4.isTypeAliasDeclaration(statement)) {
|
|
988
1259
|
return [{ name: statement.name.text, kind: "type", line, exported: isExported(statement), node: statement }];
|
|
989
1260
|
}
|
|
990
|
-
if (
|
|
1261
|
+
if (ts4.isEnumDeclaration(statement)) {
|
|
991
1262
|
return [{ name: statement.name.text, kind: "enum", line, exported: isExported(statement), node: statement }];
|
|
992
1263
|
}
|
|
993
|
-
if (
|
|
994
|
-
return statement.declarationList.declarations.filter((declaration) =>
|
|
1264
|
+
if (ts4.isVariableStatement(statement)) {
|
|
1265
|
+
return statement.declarationList.declarations.filter((declaration) => ts4.isIdentifier(declaration.name)).map((declaration) => ({
|
|
995
1266
|
name: declaration.name.text,
|
|
996
1267
|
kind: "variable",
|
|
997
1268
|
line: getLine(sourceFile, declaration),
|
|
@@ -999,7 +1270,7 @@ function getTopLevelDeclaration(sourceFile, statement) {
|
|
|
999
1270
|
node: statement
|
|
1000
1271
|
}));
|
|
1001
1272
|
}
|
|
1002
|
-
if (
|
|
1273
|
+
if (ts4.isExportDeclaration(statement)) {
|
|
1003
1274
|
return [{ name: "export declaration", kind: "export", line, exported: true, node: statement }];
|
|
1004
1275
|
}
|
|
1005
1276
|
return [];
|
|
@@ -1024,9 +1295,9 @@ function isAllowedConstantDeclaration(modelName, declaration) {
|
|
|
1024
1295
|
return false;
|
|
1025
1296
|
if ([`${modelName}Input`, `${modelName}Object`, modelName, `Light${modelName}`, `${modelName}Insight`].includes(declaration.name))
|
|
1026
1297
|
return true;
|
|
1027
|
-
if (!
|
|
1298
|
+
if (!ts4.isClassDeclaration(declaration.node))
|
|
1028
1299
|
return false;
|
|
1029
|
-
const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token ===
|
|
1300
|
+
const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token === ts4.SyntaxKind.ExtendsKeyword);
|
|
1030
1301
|
const expression = heritageClause?.types[0]?.expression;
|
|
1031
1302
|
return !!expression && expression.getText().startsWith("enumOf(");
|
|
1032
1303
|
}
|
|
@@ -1092,13 +1363,13 @@ function getModuleInfo(file) {
|
|
|
1092
1363
|
return { moduleName, fileName, kind: "database" };
|
|
1093
1364
|
}
|
|
1094
1365
|
function isExportedConst(declaration) {
|
|
1095
|
-
return declaration.exported &&
|
|
1366
|
+
return declaration.exported && ts4.isVariableStatement(declaration.node) && (declaration.node.declarationList.flags & ts4.NodeFlags.Const) !== 0;
|
|
1096
1367
|
}
|
|
1097
1368
|
function isExported(node) {
|
|
1098
|
-
return !!
|
|
1369
|
+
return !!ts4.getCombinedModifierFlags(node) && !!(ts4.getCombinedModifierFlags(node) & ts4.ModifierFlags.Export);
|
|
1099
1370
|
}
|
|
1100
1371
|
function isFunctionLikeInitializer(node) {
|
|
1101
|
-
return !!node && (
|
|
1372
|
+
return !!node && (ts4.isArrowFunction(node) || ts4.isFunctionExpression(node));
|
|
1102
1373
|
}
|
|
1103
1374
|
function getBodyFingerprint(sourceFile, node) {
|
|
1104
1375
|
if (!node)
|
|
@@ -1120,7 +1391,7 @@ async function isDirectory(absolutePath) {
|
|
|
1120
1391
|
}
|
|
1121
1392
|
}
|
|
1122
1393
|
function getScriptKind(file) {
|
|
1123
|
-
return file.endsWith(".tsx") ?
|
|
1394
|
+
return file.endsWith(".tsx") ? ts4.ScriptKind.TSX : ts4.ScriptKind.TS;
|
|
1124
1395
|
}
|
|
1125
1396
|
function getLine(sourceFile, node) {
|
|
1126
1397
|
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
@@ -1141,7 +1412,7 @@ function toPascalCase(value) {
|
|
|
1141
1412
|
return value.replace(/(^|[-_./])([a-zA-Z0-9])/g, (_, __, char) => char.toUpperCase()).replace(/[-_./]/g, "");
|
|
1142
1413
|
}
|
|
1143
1414
|
function toPosix(value) {
|
|
1144
|
-
return value.split(
|
|
1415
|
+
return value.split(path3.sep).join("/");
|
|
1145
1416
|
}
|
|
1146
1417
|
function groupBy(items, getKey) {
|
|
1147
1418
|
const grouped = new Map;
|