@akanjs/cli 3.0.0-alpha.1 → 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-yv00p0s0.js → agent.command-a3n15c6t.js} +7 -6
- package/{application.command-0qsczf4k.js → application.command-rdm3et0g.js} +5 -4
- package/{applicationBuildRunner-ct2pxyvw.js → applicationBuildRunner-6m8n71w4.js} +3 -3
- package/buildBatch.proc.js +4 -3
- package/{capacitorApp-4r9sm3qg.js → capacitorApp-mr81qdxq.js} +5 -2
- package/{cloud.command-dxfv18sa.js → cloud.command-56aeekqn.js} +8 -7
- package/{context.command-8d9e3k6b.js → context.command-ysxawe7h.js} +14 -13
- package/getDirname-m9s0cmhq.js +8 -0
- package/{guideline.command-3jbkxzh6.js → guideline.command-yqf9a372.js} +5 -4
- package/guidelines/conventions/conventions.generate.json +25 -0
- package/guidelines/conventions/conventions.instruction.md +825 -0
- package/guidelines/cssRule/cssRule.instruction.md +7 -0
- package/guidelines/framework/framework.instruction.md +1 -0
- package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +24 -0
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +974 -0
- package/incrementalBuilder.proc.js +3 -2
- package/index-1577bej2.js +7 -0
- package/{index-1pvex4az.js → index-3789fdpg.js} +13 -13
- package/{index-ec09pws3.js → index-4mgkgba3.js} +3 -3
- package/{index-bbkek1bp.js → index-65aeh789.js} +1 -1
- package/{index-7c6k4yvg.js → index-8pd02prk.js} +2 -2
- package/{index-t3723f99.js → index-9va8zayb.js} +86 -17
- package/{index-nm6wghb6.js → index-a5cs7d8r.js} +5 -5
- package/{index-e16gnn26.js → index-bzznxgcx.js} +7 -7
- package/{index-pyt2b4nw.js → index-cav1azma.js} +103 -22
- package/{index-2q1f5j9k.js → index-cv9cxs5k.js} +3 -3
- package/{index-a00jg8x8.js → index-e4b2ae6b.js} +49 -32
- package/{index-8hsbnyes.js → index-etwh7sd3.js} +4 -2
- package/{index-k95xkh4p.js → index-h5whaj26.js} +2 -2
- package/{index-tdbas9r0.js → index-j37qq1f2.js} +1 -1
- package/{index-rh16j3c7.js → index-j9cxndc0.js} +3 -2
- package/{index-rzwcpcc3.js → index-kcsa1zff.js} +1 -1
- package/{index-bekddz9q.js → index-m90fn8xe.js} +4 -4
- package/{index-yzmhasdb.js → index-mewacxye.js} +7 -7
- package/{index-zasydjxd.js → index-nwqqssnb.js} +8 -10
- package/{index-mr5s7mqd.js → index-yzmqvg23.js} +3 -3
- package/{index-hgna5rjy.js → index-yzsxc30b.js} +94 -47
- package/{index-ej426mcr.js → index-z1s5f4e0.js} +34 -6
- package/index.js +19 -18
- package/{library.command-ebdj1vnm.js → library.command-x6t29a30.js} +4 -3
- package/{localRegistry.command-gb178ne8.js → localRegistry.command-649g1n7b.js} +7 -6
- package/{module.command-9c1bnsx4.js → module.command-8w8ys5t3.js} +7 -6
- package/{package.command-t93p4jkj.js → package.command-zgy7f3fz.js} +4 -3
- package/package.json +2 -2
- package/{page.command-k3k0meh2.js → page.command-yjmnkyk2.js} +4 -3
- package/{primitive.command-de6nktdt.js → primitive.command-gfqaagsg.js} +8 -7
- package/{quality.command-z5kth2wa.js → quality.command-ec8r1dn3.js} +369 -128
- package/{repair.command-ehf2g112.js → repair.command-p9ey6mkm.js} +6 -5
- package/{routeSourceValidator-cncd92fv.js → routeSourceValidator-e4jmbfcx.js} +2 -38
- package/{scalar.command-7a2jtv33.js → scalar.command-9dhmqhtm.js} +6 -5
- package/templates/app/page/styles.css.template +3 -3
- package/templates/appSample/srvkit/AuthGuard.ts +9 -5
- package/templates/appSample/srvkit/SessionInternalArg.ts +4 -3
- package/templates/crudPages/[__model__Id]/edit/page.tsx +1 -1
- package/templates/crudPages/new/page.tsx +1 -1
- package/templates/crudSinglePage/page.tsx +1 -1
- package/templates/lib/db.ts +3 -1
- package/templates/lib/useClient.ts +9 -3
- package/templates/module/__Model__.Util.tsx +1 -1
- package/templates/module/__Model__.Zone.tsx +3 -5
- package/templates/workspaceRoot/AGENTS.md.template +0 -948
- package/templates/workspaceRoot/biome.json.template +4 -198
- package/templates/workspaceRoot/docs/AI-DEVELOPMENT.md.template +4 -1
- package/{workflow.command-e0tt0vk2.js → workflow.command-ycwc6zr6.js} +13 -12
- package/{workspace.command-fpda0evj.js → workspace.command-xhyhajxa.js} +67 -48
- package/templates/libRoot/base/baseLogic.ts +0 -14
- package/templates/libRoot/base/index.ts +0 -8
- package/templates/workspaceRoot/.cursor/rules/akan-page-routing.mdc.template +0 -43
- package/templates/workspaceRoot/.cursor/rules/akan-sync-conventions.mdc.template +0 -33
- package/templates/workspaceRoot/.cursor/rules/application-test-commands.mdc.template +0 -16
- package/templates/workspaceRoot/.cursor/rules/change-scope.mdc.template +0 -12
- package/templates/workspaceRoot/.cursor/rules/client-server-boundaries.mdc.template +0 -18
- package/templates/workspaceRoot/.cursor/rules/coding-comments.mdc.template +0 -25
- package/templates/workspaceRoot/.cursor/rules/coding-style.mdc.template +0 -44
- package/templates/workspaceRoot/.cursor/rules/domain-module-conventions.mdc.template +0 -17
- package/templates/workspaceRoot/.cursor/rules/format-lint.mdc.template +0 -12
- package/templates/workspaceRoot/.cursor/rules/lint-enforced-rules.mdc.template +0 -18
- package/templates/workspaceRoot/.cursor/rules/module-file-playbook.mdc.template +0 -61
- package/templates/workspaceRoot/.cursor/rules/naming-conventions.mdc.template +0 -15
- package/templates/workspaceRoot/.cursor/rules/react-component-style.mdc.template +0 -21
- package/templates/workspaceRoot/.cursor/rules/repo-overview.mdc.template +0 -17
- package/templates/workspaceRoot/.cursor/rules/scalar-modeling.mdc.template +0 -58
- package/templates/workspaceRoot/.cursor/rules/secrets-and-env.mdc.template +0 -13
- package/templates/workspaceRoot/.cursor/rules/service-signal-conventions.mdc.template +0 -83
- package/templates/workspaceRoot/.cursor/rules/srvkit-adapters-and-guards.mdc.template +0 -50
- package/templates/workspaceRoot/.cursor/rules/ssr-first.mdc.template +0 -59
- package/templates/workspaceRoot/.cursor/rules/typescript-imports.mdc.template +0 -16
|
@@ -7,21 +7,176 @@ import {
|
|
|
7
7
|
command,
|
|
8
8
|
runner,
|
|
9
9
|
script
|
|
10
|
-
} from "./index-
|
|
11
|
-
import
|
|
10
|
+
} from "./index-etwh7sd3.js";
|
|
11
|
+
import {
|
|
12
|
+
appRootAllowedFiles,
|
|
13
|
+
libFacetRootAllowedFiles
|
|
14
|
+
} from "./index-yzsxc30b.js";
|
|
12
15
|
import"./index-mxvakhsm.js";
|
|
16
|
+
import"./index-1577bej2.js";
|
|
13
17
|
import"./index-67546d0j.js";
|
|
14
18
|
import"./index-r24hmh0q.js";
|
|
15
19
|
|
|
16
20
|
// pkgs/@akanjs/devkit/qualityScanner.ts
|
|
17
21
|
import { createHash } from "crypto";
|
|
18
22
|
import { readdir, readFile, stat } from "fs/promises";
|
|
19
|
-
import
|
|
23
|
+
import path3 from "path";
|
|
24
|
+
import { RESERVED_ROUTE_CONFIG_EXPORTS } from "akanjs/common";
|
|
20
25
|
import ignore from "ignore";
|
|
21
|
-
import
|
|
26
|
+
import ts4 from "typescript";
|
|
22
27
|
|
|
23
|
-
// pkgs/@akanjs/devkit/
|
|
28
|
+
// pkgs/@akanjs/devkit/mcpScanner.ts
|
|
29
|
+
import path from "path";
|
|
24
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";
|
|
25
180
|
var STATIC_COMPONENT_MIN_MASS = 4;
|
|
26
181
|
var MIXED_COMPONENT_MIN_MASS = 10;
|
|
27
182
|
var MIXED_COMPONENT_MAX_TOUCHES = 2;
|
|
@@ -139,31 +294,31 @@ class SsrScanner {
|
|
|
139
294
|
});
|
|
140
295
|
}
|
|
141
296
|
}
|
|
142
|
-
|
|
297
|
+
ts2.forEachChild(node, visit);
|
|
143
298
|
};
|
|
144
|
-
|
|
299
|
+
ts2.forEachChild(sourceFile.sourceFile, visit);
|
|
145
300
|
return warnings;
|
|
146
301
|
}
|
|
147
302
|
#isMountEffect(sourceFile, node) {
|
|
148
|
-
if (!
|
|
303
|
+
if (!ts2.isCallExpression(node))
|
|
149
304
|
return false;
|
|
150
305
|
const callee = node.expression.getText(sourceFile);
|
|
151
306
|
if (callee !== "useEffect" && callee !== "useLayoutEffect")
|
|
152
307
|
return false;
|
|
153
308
|
const deps = node.arguments[1];
|
|
154
|
-
return !!deps &&
|
|
309
|
+
return !!deps && ts2.isArrayLiteralExpression(deps) && deps.elements.length === 0;
|
|
155
310
|
}
|
|
156
311
|
#getLoadCalls(sourceFile, node) {
|
|
157
312
|
const calls = [];
|
|
158
313
|
const visit = (child) => {
|
|
159
|
-
if (
|
|
314
|
+
if (ts2.isCallExpression(child)) {
|
|
160
315
|
const callee = child.expression.getText(sourceFile);
|
|
161
316
|
if (/^fetch\.[a-z]/.test(callee) || /^st\.do\.(init|get|view|load|list|count|insight)[A-Z]/.test(callee))
|
|
162
317
|
calls.push({ callee, node: child });
|
|
163
318
|
}
|
|
164
|
-
|
|
319
|
+
ts2.forEachChild(child, visit);
|
|
165
320
|
};
|
|
166
|
-
|
|
321
|
+
ts2.forEachChild(node, visit);
|
|
167
322
|
return calls;
|
|
168
323
|
}
|
|
169
324
|
#getTemplateStateWarnings(sourceFile) {
|
|
@@ -171,7 +326,7 @@ class SsrScanner {
|
|
|
171
326
|
return [];
|
|
172
327
|
const warnings = [];
|
|
173
328
|
const visit = (node) => {
|
|
174
|
-
if (
|
|
329
|
+
if (ts2.isCallExpression(node) && node.expression.getText(sourceFile.sourceFile) === "useState") {
|
|
175
330
|
warnings.push({
|
|
176
331
|
rule: "akan.ssr.template-client-state",
|
|
177
332
|
scope: "ssr",
|
|
@@ -181,9 +336,9 @@ class SsrScanner {
|
|
|
181
336
|
message: "Template holds form state in useState. Templates are store-driven and carry no local state."
|
|
182
337
|
});
|
|
183
338
|
}
|
|
184
|
-
|
|
339
|
+
ts2.forEachChild(node, visit);
|
|
185
340
|
};
|
|
186
|
-
|
|
341
|
+
ts2.forEachChild(sourceFile.sourceFile, visit);
|
|
187
342
|
return warnings;
|
|
188
343
|
}
|
|
189
344
|
#measureBalance(sourceFiles) {
|
|
@@ -224,14 +379,14 @@ class SsrScanner {
|
|
|
224
379
|
return components;
|
|
225
380
|
}
|
|
226
381
|
#getComponentNodes(statement) {
|
|
227
|
-
if (
|
|
382
|
+
if (ts2.isFunctionDeclaration(statement) && statement.body)
|
|
228
383
|
return [{ name: statement.name?.text ?? "default", node: statement.body }];
|
|
229
|
-
if (!
|
|
384
|
+
if (!ts2.isVariableStatement(statement))
|
|
230
385
|
return [];
|
|
231
386
|
return statement.declarationList.declarations.flatMap((declaration) => {
|
|
232
|
-
if (!
|
|
387
|
+
if (!ts2.isIdentifier(declaration.name) || !declaration.initializer)
|
|
233
388
|
return [];
|
|
234
|
-
if (!
|
|
389
|
+
if (!ts2.isArrowFunction(declaration.initializer) && !ts2.isFunctionExpression(declaration.initializer))
|
|
235
390
|
return [];
|
|
236
391
|
return [{ name: declaration.name.text, node: declaration.initializer }];
|
|
237
392
|
});
|
|
@@ -239,7 +394,7 @@ class SsrScanner {
|
|
|
239
394
|
#getTouches(sourceFile, node) {
|
|
240
395
|
const touches = [];
|
|
241
396
|
const visit = (child) => {
|
|
242
|
-
if (
|
|
397
|
+
if (ts2.isCallExpression(child)) {
|
|
243
398
|
const callee = child.expression.getText(sourceFile);
|
|
244
399
|
const bareName = callee.split(".").pop() ?? callee;
|
|
245
400
|
if (callee === "createContext" || callee === "lazy")
|
|
@@ -247,60 +402,60 @@ class SsrScanner {
|
|
|
247
402
|
else if (/^use[A-Z]/.test(bareName) && !SsrScanner.#serverSafeCalls.has(bareName))
|
|
248
403
|
touches.push(bareName);
|
|
249
404
|
}
|
|
250
|
-
if (
|
|
405
|
+
if (ts2.isJsxAttribute(child) && /^on[A-Z]/.test(child.name.getText(sourceFile)))
|
|
251
406
|
touches.push(child.name.getText(sourceFile));
|
|
252
|
-
if (
|
|
407
|
+
if (ts2.isPropertyAccessExpression(child)) {
|
|
253
408
|
const root = getAccessRoot(child);
|
|
254
409
|
if (root === "st")
|
|
255
410
|
touches.push("st");
|
|
256
411
|
else if (SsrScanner.#clientGlobals.has(root))
|
|
257
412
|
touches.push(root);
|
|
258
413
|
}
|
|
259
|
-
|
|
414
|
+
ts2.forEachChild(child, visit);
|
|
260
415
|
};
|
|
261
|
-
|
|
416
|
+
ts2.forEachChild(node, visit);
|
|
262
417
|
return touches;
|
|
263
418
|
}
|
|
264
419
|
#getTagNames(sourceFile, node) {
|
|
265
420
|
const tags = new Set;
|
|
266
421
|
const visit = (child) => {
|
|
267
|
-
if (
|
|
422
|
+
if (ts2.isJsxOpeningElement(child) || ts2.isJsxSelfClosingElement(child))
|
|
268
423
|
tags.add(child.tagName.getText(sourceFile).split(".")[0]);
|
|
269
|
-
|
|
424
|
+
ts2.forEachChild(child, visit);
|
|
270
425
|
};
|
|
271
|
-
|
|
426
|
+
ts2.forEachChild(node, visit);
|
|
272
427
|
return tags;
|
|
273
428
|
}
|
|
274
429
|
#getMass(node) {
|
|
275
430
|
let mass = 0;
|
|
276
431
|
const visit = (child) => {
|
|
277
|
-
if (
|
|
432
|
+
if (ts2.isJsxOpeningElement(child) || ts2.isJsxSelfClosingElement(child))
|
|
278
433
|
mass += 1;
|
|
279
|
-
|
|
434
|
+
ts2.forEachChild(child, visit);
|
|
280
435
|
};
|
|
281
|
-
|
|
436
|
+
ts2.forEachChild(node, visit);
|
|
282
437
|
return mass;
|
|
283
438
|
}
|
|
284
439
|
#hasUseClient(sourceFile) {
|
|
285
440
|
const first = sourceFile.statements[0];
|
|
286
|
-
if (!first || !
|
|
441
|
+
if (!first || !ts2.isExpressionStatement(first) || !ts2.isStringLiteral(first.expression))
|
|
287
442
|
return false;
|
|
288
443
|
return first.expression.text === "use client";
|
|
289
444
|
}
|
|
290
445
|
#hasVendorImport(sourceFile) {
|
|
291
|
-
return sourceFile.statements.some((statement) =>
|
|
446
|
+
return sourceFile.statements.some((statement) => ts2.isImportDeclaration(statement) && isVendorSpecifier(getSpecifier(statement)));
|
|
292
447
|
}
|
|
293
448
|
#getVendorNames(sourceFile) {
|
|
294
449
|
const names = new Set;
|
|
295
450
|
for (const statement of sourceFile.statements) {
|
|
296
|
-
if (!
|
|
451
|
+
if (!ts2.isImportDeclaration(statement) || !isVendorSpecifier(getSpecifier(statement)))
|
|
297
452
|
continue;
|
|
298
453
|
const clause = statement.importClause;
|
|
299
454
|
if (clause?.name)
|
|
300
455
|
names.add(clause.name.text);
|
|
301
|
-
if (clause?.namedBindings &&
|
|
456
|
+
if (clause?.namedBindings && ts2.isNamespaceImport(clause.namedBindings))
|
|
302
457
|
names.add(clause.namedBindings.name.text);
|
|
303
|
-
if (clause?.namedBindings &&
|
|
458
|
+
if (clause?.namedBindings && ts2.isNamedImports(clause.namedBindings))
|
|
304
459
|
for (const element of clause.namedBindings.elements)
|
|
305
460
|
names.add(element.name.text);
|
|
306
461
|
}
|
|
@@ -308,10 +463,10 @@ class SsrScanner {
|
|
|
308
463
|
}
|
|
309
464
|
#importsClientRuntime(sourceFile) {
|
|
310
465
|
for (const statement of sourceFile.statements) {
|
|
311
|
-
if (!
|
|
466
|
+
if (!ts2.isImportDeclaration(statement))
|
|
312
467
|
continue;
|
|
313
468
|
const bindings = statement.importClause?.namedBindings;
|
|
314
|
-
if (!bindings || !
|
|
469
|
+
if (!bindings || !ts2.isNamedImports(bindings))
|
|
315
470
|
continue;
|
|
316
471
|
if (bindings.elements.some((element) => SsrScanner.#clientRuntimeImports.has(element.name.text)))
|
|
317
472
|
return true;
|
|
@@ -362,7 +517,7 @@ function getShare(serverMass, clientMass) {
|
|
|
362
517
|
return total === 0 ? 1 : serverMass / total;
|
|
363
518
|
}
|
|
364
519
|
function getSpecifier(statement) {
|
|
365
|
-
return
|
|
520
|
+
return ts2.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : "";
|
|
366
521
|
}
|
|
367
522
|
function isVendorSpecifier(specifier) {
|
|
368
523
|
if (specifier === "" || specifier.startsWith(".") || specifier.startsWith("/"))
|
|
@@ -375,9 +530,125 @@ function isVendorSpecifier(specifier) {
|
|
|
375
530
|
}
|
|
376
531
|
function getAccessRoot(node) {
|
|
377
532
|
let current = node;
|
|
378
|
-
while (
|
|
533
|
+
while (ts2.isPropertyAccessExpression(current))
|
|
379
534
|
current = current.expression;
|
|
380
|
-
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
|
+
}
|
|
381
652
|
}
|
|
382
653
|
|
|
383
654
|
// pkgs/@akanjs/devkit/qualityScanner.ts
|
|
@@ -404,27 +675,6 @@ var SUGGESTED_RULES = [
|
|
|
404
675
|
"Move shared app utilities to apps/*/common instead of creating apps/*/base.",
|
|
405
676
|
"Avoid large mixed-purpose class files; class export files should import helpers from neighboring utility files instead of declaring them inline."
|
|
406
677
|
];
|
|
407
|
-
var APP_ROOT_FILES = new Set([
|
|
408
|
-
"akan.app.json",
|
|
409
|
-
"akan.config.ts",
|
|
410
|
-
"capacitor.config.ts",
|
|
411
|
-
"client.ts",
|
|
412
|
-
"main.ts",
|
|
413
|
-
"package.json",
|
|
414
|
-
"server.ts",
|
|
415
|
-
"tsconfig.json"
|
|
416
|
-
]);
|
|
417
|
-
var LIB_ROOT_FILES = new Set([
|
|
418
|
-
"cnst.ts",
|
|
419
|
-
"db.ts",
|
|
420
|
-
"dict.ts",
|
|
421
|
-
"option.ts",
|
|
422
|
-
"sig.ts",
|
|
423
|
-
"srv.ts",
|
|
424
|
-
"st.ts",
|
|
425
|
-
"useClient.ts",
|
|
426
|
-
"useServer.ts"
|
|
427
|
-
]);
|
|
428
678
|
var CONVENTION_SUFFIXES = [
|
|
429
679
|
".constant.ts",
|
|
430
680
|
".dictionary.ts",
|
|
@@ -433,20 +683,6 @@ var CONVENTION_SUFFIXES = [
|
|
|
433
683
|
".signal.ts",
|
|
434
684
|
".store.ts"
|
|
435
685
|
];
|
|
436
|
-
var PAGE_RESERVED_EXPORTS = new Set([
|
|
437
|
-
"pageConfig",
|
|
438
|
-
"head",
|
|
439
|
-
"metadata",
|
|
440
|
-
"generateHead",
|
|
441
|
-
"generateMetadata",
|
|
442
|
-
"fonts",
|
|
443
|
-
"manifest",
|
|
444
|
-
"theme",
|
|
445
|
-
"reconnect",
|
|
446
|
-
"wsConnect",
|
|
447
|
-
"layoutStyle",
|
|
448
|
-
"gaTrackingId"
|
|
449
|
-
]);
|
|
450
686
|
var RULE_FIXES = {
|
|
451
687
|
"akan.global.duplicate-exported-function-name": "Rename one of the exports, or if they are the same thing, extract it into one shared module and import it in both places.",
|
|
452
688
|
"akan.global.duplicate-exported-function-body": "Extract the shared implementation into a single exported helper and import it, instead of copying the body.",
|
|
@@ -469,7 +705,10 @@ var RULE_FIXES = {
|
|
|
469
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.",
|
|
470
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.",
|
|
471
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.",
|
|
472
|
-
"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."
|
|
473
712
|
};
|
|
474
713
|
function getRuleFix(rule) {
|
|
475
714
|
if (rule.startsWith("akan.convention"))
|
|
@@ -490,7 +729,9 @@ class AkanQualityScanner {
|
|
|
490
729
|
...sourceFiles.flatMap((sourceFile) => this.#scanConventionQuality(sourceFile)),
|
|
491
730
|
...sourceFiles.flatMap((sourceFile) => this.#scanLayoutQuality(sourceFile)),
|
|
492
731
|
...abstractFiles.flatMap((abstractFile) => this.#scanAbstractQuality(abstractFile)),
|
|
493
|
-
...ssr.warnings
|
|
732
|
+
...ssr.warnings,
|
|
733
|
+
...new McpScanner().scan(sourceFiles),
|
|
734
|
+
...new StoreScanner().scan(sourceFiles)
|
|
494
735
|
];
|
|
495
736
|
return {
|
|
496
737
|
workspaceRoot,
|
|
@@ -504,7 +745,7 @@ class AkanQualityScanner {
|
|
|
504
745
|
const ignoreFilter = ignore().add(await this.#readGitIgnore(workspaceRoot));
|
|
505
746
|
const files = [];
|
|
506
747
|
for (const targetRoot of ["apps", "libs"]) {
|
|
507
|
-
const absoluteTargetRoot =
|
|
748
|
+
const absoluteTargetRoot = path3.join(workspaceRoot, targetRoot);
|
|
508
749
|
if (!await isDirectory(absoluteTargetRoot))
|
|
509
750
|
continue;
|
|
510
751
|
await this.#walkTargetFiles(workspaceRoot, absoluteTargetRoot, ignoreFilter, files);
|
|
@@ -512,7 +753,7 @@ class AkanQualityScanner {
|
|
|
512
753
|
return files.sort();
|
|
513
754
|
}
|
|
514
755
|
async#readGitIgnore(workspaceRoot) {
|
|
515
|
-
const gitIgnorePath =
|
|
756
|
+
const gitIgnorePath = path3.join(workspaceRoot, ".gitignore");
|
|
516
757
|
if (!await Bun.file(gitIgnorePath).exists())
|
|
517
758
|
return [];
|
|
518
759
|
return (await readFile(gitIgnorePath, "utf8")).split(/\r?\n/);
|
|
@@ -520,8 +761,8 @@ class AkanQualityScanner {
|
|
|
520
761
|
async#walkTargetFiles(workspaceRoot, currentPath, ignoreFilter, files) {
|
|
521
762
|
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
522
763
|
for (const entry of entries) {
|
|
523
|
-
const absolutePath =
|
|
524
|
-
const relativePath = toPosix(
|
|
764
|
+
const absolutePath = path3.join(currentPath, entry.name);
|
|
765
|
+
const relativePath = toPosix(path3.relative(workspaceRoot, absolutePath));
|
|
525
766
|
if (shouldSkipPath(relativePath, entry.isDirectory(), ignoreFilter))
|
|
526
767
|
continue;
|
|
527
768
|
if (entry.isDirectory()) {
|
|
@@ -536,17 +777,17 @@ class AkanQualityScanner {
|
|
|
536
777
|
}
|
|
537
778
|
}
|
|
538
779
|
async#readSourceFile(workspaceRoot, file) {
|
|
539
|
-
const absolutePath =
|
|
780
|
+
const absolutePath = path3.join(workspaceRoot, file);
|
|
540
781
|
const content = await readFile(absolutePath, "utf8");
|
|
541
782
|
return {
|
|
542
783
|
file,
|
|
543
784
|
absolutePath,
|
|
544
785
|
content,
|
|
545
|
-
sourceFile:
|
|
786
|
+
sourceFile: ts4.createSourceFile(file, content, ts4.ScriptTarget.Latest, true, getScriptKind(file))
|
|
546
787
|
};
|
|
547
788
|
}
|
|
548
789
|
async#readTextFile(workspaceRoot, file) {
|
|
549
|
-
return { file, content: await readFile(
|
|
790
|
+
return { file, content: await readFile(path3.join(workspaceRoot, file), "utf8") };
|
|
550
791
|
}
|
|
551
792
|
#scanGlobalQuality(sourceFiles) {
|
|
552
793
|
const exportedFunctionLikes = sourceFiles.flatMap((sourceFile) => getExportedFunctionLikes(sourceFile));
|
|
@@ -670,7 +911,7 @@ class AkanQualityScanner {
|
|
|
670
911
|
const suffix = CONVENTION_SUFFIXES.find((candidate) => sourceFile.file.endsWith(candidate));
|
|
671
912
|
if (!suffix)
|
|
672
913
|
return [];
|
|
673
|
-
const modelName = toPascalCase(
|
|
914
|
+
const modelName = toPascalCase(path3.basename(sourceFile.file, suffix));
|
|
674
915
|
const warnings = [];
|
|
675
916
|
for (const declaration of getTopLevelDeclarations(sourceFile)) {
|
|
676
917
|
if (isAllowedConventionDeclaration(suffix, modelName, declaration))
|
|
@@ -681,7 +922,7 @@ class AkanQualityScanner {
|
|
|
681
922
|
severity: "warning",
|
|
682
923
|
file: sourceFile.file,
|
|
683
924
|
line: declaration.line,
|
|
684
|
-
message: `${
|
|
925
|
+
message: `${path3.basename(sourceFile.file)} should not declare top-level ${declaration.kind} "${declaration.name}". Allowed declarations: ${getConventionDescription(suffix, modelName)}.`
|
|
685
926
|
});
|
|
686
927
|
}
|
|
687
928
|
return warnings;
|
|
@@ -703,7 +944,7 @@ class AkanQualityScanner {
|
|
|
703
944
|
#scanLayoutQuality(sourceFile) {
|
|
704
945
|
const segments = sourceFile.file.split("/");
|
|
705
946
|
const warnings = [];
|
|
706
|
-
if (segments[0] === "apps" && segments.length === 3 && !
|
|
947
|
+
if (segments[0] === "apps" && segments.length === 3 && !appRootAllowedFiles.has(segments[2])) {
|
|
707
948
|
warnings.push({
|
|
708
949
|
rule: "akan.layout.app-root-file",
|
|
709
950
|
scope: "layout",
|
|
@@ -713,7 +954,7 @@ class AkanQualityScanner {
|
|
|
713
954
|
});
|
|
714
955
|
}
|
|
715
956
|
const libRootFile = getLibRootFile(sourceFile.file);
|
|
716
|
-
if (libRootFile && !
|
|
957
|
+
if (libRootFile && !libFacetRootAllowedFiles.has(libRootFile)) {
|
|
717
958
|
warnings.push({
|
|
718
959
|
rule: "akan.layout.lib-root-file",
|
|
719
960
|
scope: "layout",
|
|
@@ -789,7 +1030,7 @@ function getExportedFunctionLikes(sourceFile) {
|
|
|
789
1030
|
const declarations = [];
|
|
790
1031
|
const nameExempt = isPageRouteFile(sourceFile.file) || isUiComponentFile(sourceFile.file);
|
|
791
1032
|
for (const statement of sourceFile.sourceFile.statements) {
|
|
792
|
-
if (
|
|
1033
|
+
if (ts4.isFunctionDeclaration(statement) && statement.name && isExported(statement)) {
|
|
793
1034
|
declarations.push({
|
|
794
1035
|
name: statement.name.text,
|
|
795
1036
|
kind: "function",
|
|
@@ -799,7 +1040,7 @@ function getExportedFunctionLikes(sourceFile) {
|
|
|
799
1040
|
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, false)
|
|
800
1041
|
});
|
|
801
1042
|
}
|
|
802
|
-
if (
|
|
1043
|
+
if (ts4.isClassDeclaration(statement) && statement.name && isExported(statement)) {
|
|
803
1044
|
declarations.push({
|
|
804
1045
|
name: statement.name.text,
|
|
805
1046
|
kind: "class",
|
|
@@ -809,9 +1050,9 @@ function getExportedFunctionLikes(sourceFile) {
|
|
|
809
1050
|
duplicateNameExempt: nameExempt || isConventionDuplicateNameExempt(sourceFile.file, isEnumClassStatement(sourceFile.sourceFile, statement))
|
|
810
1051
|
});
|
|
811
1052
|
}
|
|
812
|
-
if (
|
|
1053
|
+
if (ts4.isVariableStatement(statement) && isExported(statement)) {
|
|
813
1054
|
for (const declaration of statement.declarationList.declarations) {
|
|
814
|
-
if (!
|
|
1055
|
+
if (!ts4.isIdentifier(declaration.name) || !isFunctionLikeInitializer(declaration.initializer))
|
|
815
1056
|
continue;
|
|
816
1057
|
declarations.push({
|
|
817
1058
|
name: declaration.name.text,
|
|
@@ -850,14 +1091,14 @@ function isInLibModule(file) {
|
|
|
850
1091
|
return (segments[0] === "apps" || segments[0] === "libs") && segments.includes("lib");
|
|
851
1092
|
}
|
|
852
1093
|
function isEnumClassStatement(sourceFile, statement) {
|
|
853
|
-
if (!
|
|
1094
|
+
if (!ts4.isClassDeclaration(statement))
|
|
854
1095
|
return false;
|
|
855
|
-
const heritageClause = statement.heritageClauses?.find((clause) => clause.token ===
|
|
1096
|
+
const heritageClause = statement.heritageClauses?.find((clause) => clause.token === ts4.SyntaxKind.ExtendsKeyword);
|
|
856
1097
|
const expression = heritageClause?.types[0]?.expression;
|
|
857
1098
|
return !!expression && expression.getText(sourceFile).startsWith("enumOf(");
|
|
858
1099
|
}
|
|
859
1100
|
function getExportedClassNames(sourceFile) {
|
|
860
|
-
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);
|
|
861
1102
|
}
|
|
862
1103
|
function isComponentDeclarationFile(file) {
|
|
863
1104
|
if (!file.endsWith(".tsx"))
|
|
@@ -873,7 +1114,7 @@ function isComponentDeclarationFile(file) {
|
|
|
873
1114
|
function getComponentFileDeclarations(sourceFile) {
|
|
874
1115
|
const reExportedNames = new Set;
|
|
875
1116
|
for (const statement of sourceFile.statements) {
|
|
876
|
-
if (
|
|
1117
|
+
if (ts4.isExportDeclaration(statement) && statement.exportClause && ts4.isNamedExports(statement.exportClause)) {
|
|
877
1118
|
for (const element of statement.exportClause.elements)
|
|
878
1119
|
reExportedNames.add((element.propertyName ?? element.name).text);
|
|
879
1120
|
}
|
|
@@ -883,19 +1124,19 @@ function getComponentFileDeclarations(sourceFile) {
|
|
|
883
1124
|
const isDefaultExport = isDefaultExportStatement(statement);
|
|
884
1125
|
const inlineExported = isExported(statement);
|
|
885
1126
|
const add = (name, kind, line) => declarations.push({ name, kind, line, exported: inlineExported || reExportedNames.has(name), isDefaultExport });
|
|
886
|
-
if (
|
|
1127
|
+
if (ts4.isInterfaceDeclaration(statement))
|
|
887
1128
|
add(statement.name.text, "interface", getLine(sourceFile, statement));
|
|
888
|
-
else if (
|
|
1129
|
+
else if (ts4.isTypeAliasDeclaration(statement))
|
|
889
1130
|
add(statement.name.text, "type", getLine(sourceFile, statement));
|
|
890
|
-
else if (
|
|
1131
|
+
else if (ts4.isEnumDeclaration(statement))
|
|
891
1132
|
add(statement.name.text, "enum", getLine(sourceFile, statement));
|
|
892
|
-
else if (
|
|
1133
|
+
else if (ts4.isFunctionDeclaration(statement) && statement.name)
|
|
893
1134
|
add(statement.name.text, "function", getLine(sourceFile, statement));
|
|
894
|
-
else if (
|
|
1135
|
+
else if (ts4.isClassDeclaration(statement) && statement.name)
|
|
895
1136
|
add(statement.name.text, "class", getLine(sourceFile, statement));
|
|
896
|
-
else if (
|
|
1137
|
+
else if (ts4.isVariableStatement(statement)) {
|
|
897
1138
|
for (const declaration of statement.declarationList.declarations) {
|
|
898
|
-
if (!
|
|
1139
|
+
if (!ts4.isIdentifier(declaration.name))
|
|
899
1140
|
continue;
|
|
900
1141
|
const kind = isFunctionLikeInitializer(declaration.initializer) ? "function" : "variable";
|
|
901
1142
|
add(declaration.name.text, kind, getLine(sourceFile, declaration));
|
|
@@ -907,15 +1148,15 @@ function getComponentFileDeclarations(sourceFile) {
|
|
|
907
1148
|
function getCompoundComponentNames(sourceFile) {
|
|
908
1149
|
const names = new Set;
|
|
909
1150
|
for (const statement of sourceFile.statements) {
|
|
910
|
-
if (!
|
|
1151
|
+
if (!ts4.isExpressionStatement(statement))
|
|
911
1152
|
continue;
|
|
912
1153
|
const { expression } = statement;
|
|
913
|
-
if (!
|
|
1154
|
+
if (!ts4.isBinaryExpression(expression) || expression.operatorToken.kind !== ts4.SyntaxKind.EqualsToken)
|
|
914
1155
|
continue;
|
|
915
|
-
if (!
|
|
1156
|
+
if (!ts4.isPropertyAccessExpression(expression.left) || !isPascalCaseName(expression.left.name.text))
|
|
916
1157
|
continue;
|
|
917
1158
|
names.add(expression.left.name.text);
|
|
918
|
-
if (
|
|
1159
|
+
if (ts4.isIdentifier(expression.right) && isPascalCaseName(expression.right.text))
|
|
919
1160
|
names.add(expression.right.text);
|
|
920
1161
|
}
|
|
921
1162
|
return names;
|
|
@@ -929,15 +1170,15 @@ function isRestrictedInternalKind(kind) {
|
|
|
929
1170
|
function isAllowedComponentExport(declaration, isPage) {
|
|
930
1171
|
if (isComponentValueKind(declaration.kind) && isPascalCaseName(declaration.name))
|
|
931
1172
|
return true;
|
|
932
|
-
return isPage &&
|
|
1173
|
+
return isPage && RESERVED_ROUTE_CONFIG_EXPORTS.has(declaration.name);
|
|
933
1174
|
}
|
|
934
1175
|
function isPascalCaseName(name) {
|
|
935
1176
|
return /^[A-Z]/.test(name) && !/^[A-Z0-9_]+$/.test(name);
|
|
936
1177
|
}
|
|
937
1178
|
function isDefaultExportStatement(statement) {
|
|
938
|
-
if (
|
|
1179
|
+
if (ts4.isExportAssignment(statement))
|
|
939
1180
|
return !statement.isExportEquals;
|
|
940
|
-
return !!(
|
|
1181
|
+
return !!(ts4.getCombinedModifierFlags(statement) & ts4.ModifierFlags.Default);
|
|
941
1182
|
}
|
|
942
1183
|
function getPlaceholderExportWarnings(sourceFile) {
|
|
943
1184
|
if (!sourceFile.file.endsWith("/index.ts") && !sourceFile.file.endsWith("/index.tsx"))
|
|
@@ -967,7 +1208,7 @@ function getDictionaryTextWarnings(sourceFile) {
|
|
|
967
1208
|
function getGlobalMutationWarnings(sourceFile) {
|
|
968
1209
|
const warnings = [];
|
|
969
1210
|
for (const statement of sourceFile.sourceFile.statements) {
|
|
970
|
-
if (
|
|
1211
|
+
if (ts4.isModuleDeclaration(statement) && statement.name.getText(sourceFile.sourceFile) === "global") {
|
|
971
1212
|
warnings.push({
|
|
972
1213
|
rule: "akan.file.global-declaration",
|
|
973
1214
|
scope: "file",
|
|
@@ -977,7 +1218,7 @@ function getGlobalMutationWarnings(sourceFile) {
|
|
|
977
1218
|
message: "Global declarations require an explicit low-level integration allowlist."
|
|
978
1219
|
});
|
|
979
1220
|
}
|
|
980
|
-
if (
|
|
1221
|
+
if (ts4.isInterfaceDeclaration(statement) && statement.name.text === "Window") {
|
|
981
1222
|
warnings.push({
|
|
982
1223
|
rule: "akan.file.window-augmentation",
|
|
983
1224
|
scope: "file",
|
|
@@ -987,7 +1228,7 @@ function getGlobalMutationWarnings(sourceFile) {
|
|
|
987
1228
|
message: "Window augmentation should be isolated to approved browser integration files."
|
|
988
1229
|
});
|
|
989
1230
|
}
|
|
990
|
-
if (
|
|
1231
|
+
if (ts4.isExpressionStatement(statement) && statement.expression.getText(sourceFile.sourceFile).includes(".prototype.")) {
|
|
991
1232
|
warnings.push({
|
|
992
1233
|
rule: "akan.file.prototype-mutation",
|
|
993
1234
|
scope: "file",
|
|
@@ -1005,23 +1246,23 @@ function getTopLevelDeclarations(sourceFile) {
|
|
|
1005
1246
|
}
|
|
1006
1247
|
function getTopLevelDeclaration(sourceFile, statement) {
|
|
1007
1248
|
const line = getLine(sourceFile, statement);
|
|
1008
|
-
if (
|
|
1249
|
+
if (ts4.isClassDeclaration(statement) && statement.name) {
|
|
1009
1250
|
return [{ name: statement.name.text, kind: "class", line, exported: isExported(statement), node: statement }];
|
|
1010
1251
|
}
|
|
1011
|
-
if (
|
|
1252
|
+
if (ts4.isFunctionDeclaration(statement) && statement.name) {
|
|
1012
1253
|
return [{ name: statement.name.text, kind: "function", line, exported: isExported(statement), node: statement }];
|
|
1013
1254
|
}
|
|
1014
|
-
if (
|
|
1255
|
+
if (ts4.isInterfaceDeclaration(statement)) {
|
|
1015
1256
|
return [{ name: statement.name.text, kind: "interface", line, exported: isExported(statement), node: statement }];
|
|
1016
1257
|
}
|
|
1017
|
-
if (
|
|
1258
|
+
if (ts4.isTypeAliasDeclaration(statement)) {
|
|
1018
1259
|
return [{ name: statement.name.text, kind: "type", line, exported: isExported(statement), node: statement }];
|
|
1019
1260
|
}
|
|
1020
|
-
if (
|
|
1261
|
+
if (ts4.isEnumDeclaration(statement)) {
|
|
1021
1262
|
return [{ name: statement.name.text, kind: "enum", line, exported: isExported(statement), node: statement }];
|
|
1022
1263
|
}
|
|
1023
|
-
if (
|
|
1024
|
-
return statement.declarationList.declarations.filter((declaration) =>
|
|
1264
|
+
if (ts4.isVariableStatement(statement)) {
|
|
1265
|
+
return statement.declarationList.declarations.filter((declaration) => ts4.isIdentifier(declaration.name)).map((declaration) => ({
|
|
1025
1266
|
name: declaration.name.text,
|
|
1026
1267
|
kind: "variable",
|
|
1027
1268
|
line: getLine(sourceFile, declaration),
|
|
@@ -1029,7 +1270,7 @@ function getTopLevelDeclaration(sourceFile, statement) {
|
|
|
1029
1270
|
node: statement
|
|
1030
1271
|
}));
|
|
1031
1272
|
}
|
|
1032
|
-
if (
|
|
1273
|
+
if (ts4.isExportDeclaration(statement)) {
|
|
1033
1274
|
return [{ name: "export declaration", kind: "export", line, exported: true, node: statement }];
|
|
1034
1275
|
}
|
|
1035
1276
|
return [];
|
|
@@ -1054,9 +1295,9 @@ function isAllowedConstantDeclaration(modelName, declaration) {
|
|
|
1054
1295
|
return false;
|
|
1055
1296
|
if ([`${modelName}Input`, `${modelName}Object`, modelName, `Light${modelName}`, `${modelName}Insight`].includes(declaration.name))
|
|
1056
1297
|
return true;
|
|
1057
|
-
if (!
|
|
1298
|
+
if (!ts4.isClassDeclaration(declaration.node))
|
|
1058
1299
|
return false;
|
|
1059
|
-
const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token ===
|
|
1300
|
+
const heritageClause = declaration.node.heritageClauses?.find((clause) => clause.token === ts4.SyntaxKind.ExtendsKeyword);
|
|
1060
1301
|
const expression = heritageClause?.types[0]?.expression;
|
|
1061
1302
|
return !!expression && expression.getText().startsWith("enumOf(");
|
|
1062
1303
|
}
|
|
@@ -1122,13 +1363,13 @@ function getModuleInfo(file) {
|
|
|
1122
1363
|
return { moduleName, fileName, kind: "database" };
|
|
1123
1364
|
}
|
|
1124
1365
|
function isExportedConst(declaration) {
|
|
1125
|
-
return declaration.exported &&
|
|
1366
|
+
return declaration.exported && ts4.isVariableStatement(declaration.node) && (declaration.node.declarationList.flags & ts4.NodeFlags.Const) !== 0;
|
|
1126
1367
|
}
|
|
1127
1368
|
function isExported(node) {
|
|
1128
|
-
return !!
|
|
1369
|
+
return !!ts4.getCombinedModifierFlags(node) && !!(ts4.getCombinedModifierFlags(node) & ts4.ModifierFlags.Export);
|
|
1129
1370
|
}
|
|
1130
1371
|
function isFunctionLikeInitializer(node) {
|
|
1131
|
-
return !!node && (
|
|
1372
|
+
return !!node && (ts4.isArrowFunction(node) || ts4.isFunctionExpression(node));
|
|
1132
1373
|
}
|
|
1133
1374
|
function getBodyFingerprint(sourceFile, node) {
|
|
1134
1375
|
if (!node)
|
|
@@ -1150,7 +1391,7 @@ async function isDirectory(absolutePath) {
|
|
|
1150
1391
|
}
|
|
1151
1392
|
}
|
|
1152
1393
|
function getScriptKind(file) {
|
|
1153
|
-
return file.endsWith(".tsx") ?
|
|
1394
|
+
return file.endsWith(".tsx") ? ts4.ScriptKind.TSX : ts4.ScriptKind.TS;
|
|
1154
1395
|
}
|
|
1155
1396
|
function getLine(sourceFile, node) {
|
|
1156
1397
|
return sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1;
|
|
@@ -1171,7 +1412,7 @@ function toPascalCase(value) {
|
|
|
1171
1412
|
return value.replace(/(^|[-_./])([a-zA-Z0-9])/g, (_, __, char) => char.toUpperCase()).replace(/[-_./]/g, "");
|
|
1172
1413
|
}
|
|
1173
1414
|
function toPosix(value) {
|
|
1174
|
-
return value.split(
|
|
1415
|
+
return value.split(path3.sep).join("/");
|
|
1175
1416
|
}
|
|
1176
1417
|
function groupBy(items, getKey) {
|
|
1177
1418
|
const grouped = new Map;
|